From 561b5befac2abf2251953fff734c7956029b739f Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 14 Jul 2020 11:05:09 -0400 Subject: [PATCH 01/82] [Ingest Manager] Enable ingest manager plugin by default. (#70955) --- .../settings/ingest-manager-settings.asciidoc | 5 +-- x-pack/plugins/ingest_manager/README.md | 4 +- .../plugins/ingest_manager/public/plugin.ts | 45 ++++++++++--------- x-pack/plugins/ingest_manager/server/index.ts | 2 +- .../ingest_manager/server/routes/app/index.ts | 4 +- .../public/app/home/setup.tsx | 2 +- .../mock/endpoint/dependencies_start_mock.ts | 5 ++- 7 files changed, 37 insertions(+), 30 deletions(-) diff --git a/docs/settings/ingest-manager-settings.asciidoc b/docs/settings/ingest-manager-settings.asciidoc index 604471edc4d59..30e11f726c26b 100644 --- a/docs/settings/ingest-manager-settings.asciidoc +++ b/docs/settings/ingest-manager-settings.asciidoc @@ -8,8 +8,7 @@ experimental[] You can configure `xpack.ingestManager` settings in your `kibana.yml`. -By default, {ingest-manager} is not enabled. You need to -enable it. To use {fleet}, you also need to configure {kib} and {es} hosts. +By default, {ingest-manager} is enabled. To use {fleet}, you also need to configure {kib} and {es} hosts. See the {ingest-guide}/index.html[Ingest Management] docs for more information. @@ -19,7 +18,7 @@ See the {ingest-guide}/index.html[Ingest Management] docs for more information. [cols="2*<"] |=== | `xpack.ingestManager.enabled` {ess-icon} - | Set to `true` to enable {ingest-manager}. + | Set to `true` (default) to enable {ingest-manager}. | `xpack.ingestManager.fleet.enabled` {ess-icon} | Set to `true` (default) to enable {fleet}. |=== diff --git a/x-pack/plugins/ingest_manager/README.md b/x-pack/plugins/ingest_manager/README.md index 1a19672331035..a523ddeb7c499 100644 --- a/x-pack/plugins/ingest_manager/README.md +++ b/x-pack/plugins/ingest_manager/README.md @@ -2,8 +2,8 @@ ## Plugin -- The plugin is disabled by default. See the TypeScript type for the [the available plugin configuration options](https://github.com/elastic/kibana/blob/master/x-pack/plugins/ingest_manager/common/types/index.ts#L9-L27) -- Setting `xpack.ingestManager.enabled=true` enables the plugin including the EPM and Fleet features. It also adds the `PACKAGE_CONFIG_API_ROUTES` and `AGENT_CONFIG_API_ROUTES` values in [`common/constants/routes.ts`](./common/constants/routes.ts) +- The plugin is enabled by default. See the TypeScript type for the [the available plugin configuration options](https://github.com/elastic/kibana/blob/master/x-pack/plugins/ingest_manager/common/types/index.ts#L9-L27) +- Adding `xpack.ingestManager.enabled=false` will disable the plugin including the EPM and Fleet features. It will also remove the `PACKAGE_CONFIG_API_ROUTES` and `AGENT_CONFIG_API_ROUTES` values in [`common/constants/routes.ts`](./common/constants/routes.ts) - Adding `--xpack.ingestManager.fleet.enabled=false` will disable the Fleet API & UI - [code for adding the routes](https://github.com/elastic/kibana/blob/1f27d349533b1c2865c10c45b2cf705d7416fb36/x-pack/plugins/ingest_manager/server/plugin.ts#L115-L133) - [Integration tests](server/integration_tests/router.test.ts) diff --git a/x-pack/plugins/ingest_manager/public/plugin.ts b/x-pack/plugins/ingest_manager/public/plugin.ts index 172ad2df210c3..670e75f7a241b 100644 --- a/x-pack/plugins/ingest_manager/public/plugin.ts +++ b/x-pack/plugins/ingest_manager/public/plugin.ts @@ -39,7 +39,7 @@ export interface IngestManagerSetup {} */ export interface IngestManagerStart { registerPackageConfigComponent: typeof registerPackageConfigComponent; - success: Promise; + isInitialized: () => Promise; } export interface IngestManagerSetupDeps { @@ -100,27 +100,32 @@ export class IngestManagerPlugin } public async start(core: CoreStart): Promise { - let successPromise: IngestManagerStart['success']; - try { - const permissionsResponse = await core.http.get( - appRoutesService.getCheckPermissionsPath() - ); - - if (permissionsResponse?.success) { - successPromise = core.http - .post(setupRouteService.getSetupPath()) - .then(({ isInitialized }) => - isInitialized ? Promise.resolve(true) : Promise.reject(new Error('Unknown setup error')) - ); - } else { - throw new Error(permissionsResponse?.error || 'Unknown permissions error'); - } - } catch (error) { - successPromise = Promise.reject(error); - } + let successPromise: ReturnType; return { - success: successPromise, + isInitialized: () => { + if (!successPromise) { + successPromise = Promise.resolve().then(async () => { + const permissionsResponse = await core.http.get( + appRoutesService.getCheckPermissionsPath() + ); + + if (permissionsResponse?.success) { + return core.http + .post(setupRouteService.getSetupPath()) + .then(({ isInitialized }) => + isInitialized + ? Promise.resolve(true) + : Promise.reject(new Error('Unknown setup error')) + ); + } else { + throw new Error(permissionsResponse?.error || 'Unknown permissions error'); + } + }); + } + + return successPromise; + }, registerPackageConfigComponent, }; } diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index 1823cc3561693..16c0b6449d1e8 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -20,7 +20,7 @@ export const config = { fleet: true, }, schema: schema.object({ - enabled: schema.boolean({ defaultValue: false }), + enabled: schema.boolean({ defaultValue: true }), registryUrl: schema.maybe(schema.uri()), fleet: schema.object({ enabled: schema.boolean({ defaultValue: true }), diff --git a/x-pack/plugins/ingest_manager/server/routes/app/index.ts b/x-pack/plugins/ingest_manager/server/routes/app/index.ts index 9d666efc7e9ce..ce2bf6fcdaf17 100644 --- a/x-pack/plugins/ingest_manager/server/routes/app/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/app/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { IRouter, RequestHandler } from 'src/core/server'; -import { PLUGIN_ID, APP_API_ROUTES } from '../../constants'; +import { APP_API_ROUTES } from '../../constants'; import { appContextService } from '../../services'; import { CheckPermissionsResponse } from '../../../common'; @@ -37,7 +37,7 @@ export const registerRoutes = (router: IRouter) => { { path: APP_API_ROUTES.CHECK_PERMISSIONS_PATTERN, validate: {}, - options: { tags: [`access:${PLUGIN_ID}-read`] }, + options: { tags: [] }, }, getCheckPermissionsHandler ); diff --git a/x-pack/plugins/security_solution/public/app/home/setup.tsx b/x-pack/plugins/security_solution/public/app/home/setup.tsx index bf7ce2ddf8b50..3f4b0c19e7035 100644 --- a/x-pack/plugins/security_solution/public/app/home/setup.tsx +++ b/x-pack/plugins/security_solution/public/app/home/setup.tsx @@ -32,7 +32,7 @@ export const Setup: React.FunctionComponent<{ }); }; - ingestManager.success.catch((error: Error) => displayToastWithModal(error.message)); + ingestManager.isInitialized().catch((error: Error) => displayToastWithModal(error.message)); }, [ingestManager, notifications.toasts]); return null; diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts b/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts index 9276d503176c6..9cf99f2442aab 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts @@ -59,6 +59,9 @@ export const depsStartMock: () => DepsStartMock = () => { return { data: dataMock, - ingestManager: { success: Promise.resolve(true), registerPackageConfigComponent }, + ingestManager: { + isInitialized: () => Promise.resolve(true), + registerPackageConfigComponent, + }, }; }; From 7d57be6d80f77f017fef767b637fa5e7420b6aa6 Mon Sep 17 00:00:00 2001 From: Diana Derevyankina <54894989+DziyanaDzeraviankina@users.noreply.github.com> Date: Tue, 14 Jul 2020 18:11:00 +0300 Subject: [PATCH 02/82] Convert vega tests to jest (#71073) * Convert vega tests to jest Part of #57813 * Remove unused config * Move assets to __test__ folder and remove unnecessary code * clenup * cleanup * Revert default.spec.hjson file and mock default_spec * Refactor some code Co-authored-by: Alexey Antonov --- .../__tests__/vis_type_vega/vega_graph.hjson | 76 ---- .../vis_type_vega/vega_image_512.png | Bin 30730 -> 0 bytes .../vis_type_vega/vega_map_image_256.png | Bin 1671 -> 0 bytes .../vis_type_vega/vega_map_test.hjson | 20 - .../vis_type_vega/vega_tooltip_test.hjson | 44 --- .../vis_type_vega/vega_visualization.js | 362 ------------------ .../vis_type_vega/vegalite_graph.hjson | 45 --- .../vis_type_vega/vegalite_image_256.png | Bin 9287 -> 0 bytes .../vis_type_vega/vegalite_image_512.png | Bin 23449 -> 0 bytes .../tag_cloud_visualization.test.js | 2 - .../public/__mocks__/services.ts | 57 --- .../vega_visualization.test.js.snap | 9 + .../vis_type_vega/public/default_spec.ts | 23 ++ .../public/test_utils/default.spec.json | 41 ++ .../public/test_utils/vega_graph.json | 78 ++++ .../public/test_utils/vega_map_test.json | 20 + .../public/test_utils/vegalite_graph.json | 42 ++ src/plugins/vis_type_vega/public/vega_type.ts | 5 +- .../public/vega_visualization.test.js | 232 +++++++++++ 19 files changed, 447 insertions(+), 609 deletions(-) delete mode 100644 src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_graph.hjson delete mode 100644 src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_image_512.png delete mode 100644 src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_image_256.png delete mode 100644 src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_test.hjson delete mode 100644 src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_tooltip_test.hjson delete mode 100644 src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js delete mode 100644 src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_graph.hjson delete mode 100644 src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_image_256.png delete mode 100644 src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_image_512.png delete mode 100644 src/plugins/vis_type_vega/public/__mocks__/services.ts create mode 100644 src/plugins/vis_type_vega/public/__snapshots__/vega_visualization.test.js.snap create mode 100644 src/plugins/vis_type_vega/public/default_spec.ts create mode 100644 src/plugins/vis_type_vega/public/test_utils/default.spec.json create mode 100644 src/plugins/vis_type_vega/public/test_utils/vega_graph.json create mode 100644 src/plugins/vis_type_vega/public/test_utils/vega_map_test.json create mode 100644 src/plugins/vis_type_vega/public/test_utils/vegalite_graph.json create mode 100644 src/plugins/vis_type_vega/public/vega_visualization.test.js diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_graph.hjson b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_graph.hjson deleted file mode 100644 index db19c937ca990..0000000000000 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_graph.hjson +++ /dev/null @@ -1,76 +0,0 @@ -{ - // Adapted from Vega's https://vega.github.io/vega/examples/stacked-area-chart/ - - $schema: https://vega.github.io/schema/vega/v5.json - data: [ - { - name: table - values: [ - {x: 0, y: 28, c: 0}, {x: 0, y: 55, c: 1}, {x: 1, y: 43, c: 0}, {x: 1, y: 91, c: 1}, - {x: 2, y: 81, c: 0}, {x: 2, y: 53, c: 1}, {x: 3, y: 19, c: 0}, {x: 3, y: 87, c: 1}, - {x: 4, y: 52, c: 0}, {x: 4, y: 48, c: 1}, {x: 5, y: 24, c: 0}, {x: 5, y: 49, c: 1}, - {x: 6, y: 87, c: 0}, {x: 6, y: 66, c: 1}, {x: 7, y: 17, c: 0}, {x: 7, y: 27, c: 1}, - {x: 8, y: 68, c: 0}, {x: 8, y: 16, c: 1}, {x: 9, y: 49, c: 0}, {x: 9, y: 15, c: 1} - ] - transform: [ - { - type: stack - groupby: ["x"] - sort: {field: "c"} - field: y - } - ] - } - ] - scales: [ - { - name: x - type: point - range: width - domain: {data: "table", field: "x"} - } - { - name: y - type: linear - range: height - nice: true - zero: true - domain: {data: "table", field: "y1"} - } - { - name: color - type: ordinal - range: category - domain: {data: "table", field: "c"} - } - ] - marks: [ - { - type: group - from: { - facet: {name: "series", data: "table", groupby: "c"} - } - marks: [ - { - type: area - from: {data: "series"} - encode: { - enter: { - interpolate: {value: "monotone"} - x: {scale: "x", field: "x"} - y: {scale: "y", field: "y0"} - y2: {scale: "y", field: "y1"} - fill: {scale: "color", field: "c"} - } - update: { - fillOpacity: {value: 1} - } - hover: { - fillOpacity: {value: 0.5} - } - } - } - ] - } - ] -} diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_image_512.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_image_512.png deleted file mode 100644 index cc28886794f0359da3097fa2320dcc35b3257ff8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30730 zcmeFY^;eYd7dHCLFu)8b3?Lv%3?QMTbTc&4-6Q9xrh7$y!rB;bgd!cS;g4AD(RV(HvQcukS((^;)wM8osVknz`Xd2-VRtu;!^d{7 zKleia&H2DlTv0u*&#v4&F_{Ha1%u#*Fe62vw_9SGEts*eOyK&jfG;o~`|l_4FK9So zA3%1Mcz*fsYb*@>)$#xS`hQ3R6@?D^*kMa1JIIsrYmboNi)ALp5fql&O%8lps0vC2 z#%J}V7)U+=_--Mqm*`O{Ge>rnV!&~|YehDA57F=uers2Bp<_bQCmD<^&ExyxB=ZnV zHR1KWps8S+-^UuG_Lpd7_K_NRhEKh70Aj#}Z1ou22rM0o>oZ3~)ey8@59}w*#^@xe z8K$a;kr=>DGF`#@o1Yr1bpz*lpjsgDvW!u-o{nZtz`Jox6qd$``4gE9PW8$~l5!AS zNEp_Dx0R1}bD4X{?YQaQm@BsZ6*i5+;$ZIT2s4A~5z69LR{NJ4e>;-k{w*m#h1U{W z#nMib&k??I7<`~w(UTTj2r7qm{G5q4&^OTR6VuLbEZ{bN_ZT{hoQGiV4&+84xI+Pw zQo-ywqIof8U|70O8q>kQxv4-24|Ws`dXU> z&O{v=dzXhr0Zzx&!u{#2-}<~>?yMGv%qwK8%>4Q0d6Y`>%rpywgo*+1&d*ZU za>9Xp`z%3--s60u7h!Q&n3{c`1@DXpLUKKmRI=5znfpRBW}E)rW^t%y`=H^`Zd+oT zu}>j2nI#O6359_|2$TZ9!omnp8u&OET}L>VKXuweZpVI0f9z<@Vij&}^_WQ16ZY z_awg}q1wSyBcpVLalNlH(Zk@mSnp7YUY|<7>upK4#2HqF}$T>Dz0WBM$`?9VaDR1hd)n*g1hA=X<~or_a^?hbv=8> zz==Bl25a_%Ubg4pKDvh<8-qKQRw$VLY$k>7xMe0JOvkTXfAv6&6W>qJ=xyHFJWK^f9=dRH}b!5jpZn zBQK(}W5}FK)vA8Vi{EPtZTq*>&3{V~!({1z5i)DovGMDwn5oB^qF-P!pwB>))J&v; z8{S8{EdT|1^NpD;BI05?&01M-&{VVq4G2M@d`BXK0S&G+n$Pb?r#%lM@Qd%~MucC> z{*^sHWgmEwhcFW%ptScd3HIp1C{fZhPT-vll;L`cFos?*V4*Z%n{g|>49jEwTDKN5 z`gP7Z)jixvncZ%KI&kdqFVIoTp?T}_Aq8h*kY{dfSvnnCk2ux3z_b1@m<@@B`hPr? zfcXAa&jNOxzmF0&(Wzh^p@H~V$Hm-4&_Qn?p2)AN1M6R;#NqqWudCXX{aM+$-LA!D zqTGzS78g?i)fgU-sppaly>Qmne~tUIj*1L)XFs56tM|D-O!v=s7-hh^IbH+)xNQaj>fVIdU1{vrgy4S+Kcu@q409g$!ka+RUF@_LFy zroZOq#)1wGJN_?6m{)}!^n7$>iE1}Mpa|qyIF$058%djm?S!t=g@Ja@QE-{RXmFW! z3_w1ak^fov&3*ne^Xc3FKmVST2fET=0?b@#xCTS+RB8-Yg{;c}l^S@~@cKOyeo$oM z$f3CYIbdHV4I=}{r&N_ZNY3$me(fR_wng>7f$E{22z(#Ylg-pX2+8iFwUsV>nLIoP z+(64j4A;|oA{Sh9 zv^Jf|$*|E`m_fvojqTSMFC&|;1^(LVnQ5cg$&b<~HykXJJsXMUyn^ihWewl28WA6y z?}Ow~9IG_l(i1_P&Fo-!n888Id?nDT;_W=JRl0C#ww%aE+>%CV;9$}R0&$V9_lx$i zQ>YS}t`8Jj^|XY6l_n@M))Z_;eE+hTg=3Z_tZB(rMLakegQgBc{8dJAxu#KoZJi(WAzqabL< z8UdWnq9O0<{je+h%}u|EoO&iQI3=W#J_lNeOR6(tySXWZ@Tv(%BKI$V0;)+#$PkOn zQ2B2qS(ThG$GR)Hmafc56{xrUmY!^r$@5JI2EOH0;phvVCzdxmndi44pd${B#8eSK zjfLq&{?+BpIxfaWQO6))VW5})2Dqi)=w_YIZBop?sJLNdAX9@lehLhISC)i5x#F1E zAu)dm_1a|+|5bDRnK~J9oPln$Y*PYWgNXzEasOiKsRC6)&2tN~{T1Na_l+!z9C0yX zj_^K+yXYuvIq3|vF;dOV%X=k!ft}0GFB+#O#xHxMiQ&w{tzo{ZDYRD+b3%1yS0vzg z4OfFX79d#v-KVX1B<&NW-(^-zi&pP3BtxrqI#Cw#*>y-4fs{>Z`@c0wSknC&pn#)B zO$s-i6zy&CVNuRf)}A+QUM|$EW|dlttW>a~niTu(cT`bGrvIYM?{!NiDW>@j2`Vo)Kz?9kF13;|(l_5p zladkoYNxg1*V{=pxrk*U6|5_akMWEfc*&9M0*2aBS$_j1G~(D6Xj(3bRFGqBNrlf+ z44LVavd{WANY4vb-n&jpLBm5u=L%X+@Xh3AJV{m}i$zdF=57FBP!2$Qna+!#WTXg! zM{@68mEX`Zz#-dzff118rDkZF`kEWDEd8neZptsFh>5zJWUOfzE<8-^s-Sw5d@c)M z@whCa&fFZfPgg>_CZ`aX9G-v|E9BO3^WA>{|V;=M|3tSsK46| z`%$??>PP&-oq!pI3*m6SfFi5XrTZ-zkn_9prWd(REe7f}a79UA|9uECm(ai5X>Q$8 zUacGUK&fE1=28iut16A6eZhz&i*NCd`QP{AfJw09pYSTR;KRKIrg2hlyxib3-8ahh z9B(5VJ>McphvHNSfw4Y45y-a7_tE6KjmP4h45$MDJ9o+EQF}2lGU}>-kC(*;N=usQ zvlJ8h;k79ZZ3#PPnkplh2OloxPGlo)siA*D<`?f?J`~V0z2K`p=W5MjU`JR;vJPyh zdfIicGSGq{vW^tgIkj;3)2sO8-%CKdYb|YtUViG3fsQ7uhz4kANQ1#%mpV)sz#$Ax zmG_Iy=hx=ziz z2a|>!n|3oQ1(HyspQm)_W2um*V* zIzp^BU|UGV0+-GX+gGH?!O4yzUlKbm-g^34afY_7XW|RRq4;K*dQ*c`GCtu`JfTGO#%Zr83E6Q-HS$msFXVWsT(Wufvw9fIAg)(9X4IMAK^r-*~*e%LW}S zPE-IsMn{|^262M$$o`^JsSWQsv^i!K>AZL+>P zM4w?|V47Ud3)nYC-d9$W0~~X_yyd?@CXx|OcSuo*rPgJ_~KUb%c z^MvLhMyS5g$+^+)gjC=LbYxH2l9tUFt1m0^AVa{415C2qsku*M8~Ob zfO^_;2N}GO;lLfHeRu7X@kL>V_h}@$8~;BiB%3=5=bn(9=&BrntTc3=R`hnMhL#_( zfl%w56bSO1<`=&0+VWs_TYt8~*bGWe2Oc)yK5$`VCR?%n1PXHtNU6G{OtFLp-vdK| zG90x)xuaObUqY%HJp5JULZ;w14qC*M3zfG=XI;^d10;KTXgeIHs60a)$Ae-v1}q9) zy%^2xn3v{`;>{>F??TCG0M2Hc?tA^-l~S0{cKROaITU)xqD1GVJ2ymoGiF)kg+M~IHuTk-<5{9&N4xWf2+iO}D$Im1t+s6U( zfHQGa%K)(v@wl%=pz+^4Lav63lAMD+)q=V1&(#ohUS%n2%{R5&!j_Duz-R zr}OceY~^u&OHO*`@~sNR(?QVm9LzU zi*8G94g*5&H}})7&|?|FsPmkzvEJ4X;VS_=v1dcTpSLO+7)HaW(PG0YJ^5V2`7@cA zau}!w5|Did{gfth>Q|#GZ?$I#2HihC!v29^^sn%bq!~Y5IV<&a&`uLH^GK4Fy4LKn zge9Id;%V!PUj^*w>s5T+<>ND#=4RpNxnAnT)z(n& zo0ICO;e2HxRK#nZ!4W%-Bx!K0Cp~rs#QefkjkU}nr29_5P~5f$uo%yN?)Nga@U$7{!%wy5wHCq; zO76CF%%~Tjp4o(2q<_vNk8121870<1alN_03Q@DGoR| zkD+PI*O;xmY>BP%b?bT4myxB$m2L}H=KU*J>m6W~ROmbr6{tU}%=lZEWw7Hk?%4v{ zOSbd~?8oFm(ANlG9stC=iNNvw9y$qVxSO}O)=Rqf zzUIe<@F#?o+Sup4*2CHfME^rW?P9|dy-ei;>%l5W7Z$Gb!Y+XA3>OiVK`#n5)lC?An0pDhAo}3^vI) zw6ZS$OOGk?=&({Hni$ZC&k&yZD%T-+@M&!(b};#)EOrHg1*wHVK^1iK*en7y zbj7zBO~=CsLgCO@N-;kE!cPMJX@eJHP17Swb}jjlyy2mkDIBa8bUv25w zH*|kB_tPP?ALP(dRSO0_2O~o-HJ_sVpiX?=-q~Uv2GRM5CIJApaI7jRsAsejHJ
(OVW zE5!hMTR}#w{xhAFFw+zYCdnp#MSgdVH2V2PAgPTqYM@%hFjv*t9Z1dQI(GSGmYf8I(KgXkicDv)aPGy@JWK++)q1BR>?L{ zc7u;_8F<{e-}kz+Y&~8nYCR-JE4muLlC!Ee09theOyb($fa_5^^}sqWG(a&rxWIll zYi67#dArvA08id~nZq)Ls84JyhD0l+~Zp$Z4i;}f{5!w@JeW8;Sk-s%s# zB5y2M1gxPKNYGiEF@(LxCB8LTk{5+n%~|!?5$bXSs0OgV%qY7~&ebsg$fA=rf-x>) zNM)x7=2OU*6r;b8?@aY(;0Y;X3gN- z3&}(q!Z_RfCRJGS&2{yVSubgXMq8DFOl+eoRD+?9@iBO_m-?6KUn`IyuAA->n>wo$ zC$h#tEhj1A1Nngr@1JDFK=TdxGRD8%x|u+efC5*$4l<~)*vy9@{sTy+CsVb!I7jm@ zGSPxyo)wIP3BF@+2i}sUa~En%N4biCyaMBI(hN*iTTFA{QJ9KFa#?u>ZZo>-n<=OE=T8h)P?bAsBxwx$sSn$Kjx5=$U1+!djvDn3T~3UTcnu;X8FTjbIGK+?M^ym0fT zk{394=^^1Jan5tBD%9dv$ zdJ+d)Oqh|kF>$DMbkD<@c}NrjH7EzB6$LKFKsv!rZ-LpZGoB5PDCVhmcqo+Qy!C!@ zFyoCzkN90AYXAVk)Asl%NiPLj1sjlyM?2woTq@o6D`0$oQGH|*w9p9^pJYeFI3L#S z>06wlkv zARDB-#lVpD@y2JG)<7FoHE@8Ry)xG!*y}f7IGQ|oO>df<;5U5cs%n)rt z6+d4$wf4#UsUV|5^}YXy<-qqZ?udPjZ<~=)v5!g9Sr~qoo#Nv#erTxj7gWG63^u)K~?QPUuO3 zbAIta=*T9BV1tZ{p6G!;0uKhAf3AqRnr3`HYbqI18o#k+2>ALK0J1O7kEg0d>=X<( zS46r=SzQ6{3Ub(faP14BJ7h^=SY_Z?4BCobZ$)a(N0<*jsz{Tjid|0wxn>avJ>OhR zbZ$zHR(<}mau_02x?uS~)`3;aw5=nxX!|29@DZ?pByB~{$Uv|^Gz*zJ)R@fnZ8#ve zf;Qrn6Wt3BTY&U2$482NZ|-QlxTs?xYa}kA>yc%uH;M%7oq_&ObWFz zly%qpJywyn^(uWJ7?wm^kqh19O$qbNS?!_p&(Gq;3q^mqzPp)#D{~qc+ysXVsc1n4 zSd;wf42}6S|IL#$Fhd13-7anaN-QNj*mg78)jvG*2P_U81~Pu)a=_lr+r*ra2sk4e zHr3LR%+U2rS67C%btfjpvn$NA;5~Ba#+CqZTMfp3X7kdiw`nPd-SE_e@OX1I^bdI#nerP!!!90nv66Nxzq1qN_T*+ z1pGNaugtFkO$aHE;&Fdm@H-B2DZzH{E$gckMX=;J;n~LTl&Wq^#b+F#`3= zlg00>uWy+J`-X~OdzeX3TujNma6y#(MZs5M}TbdZnr&|z(> zvXN%yWG3RTDgfbuanFs6o@MF&f^FX6$)I1@=?*xZ!y#WO>S?a;a)%aPO)=@>f`g4@ zFFDSAdE#^I9TNVNA1_SXz=DAKxm-^J>_N@ z(Q0?2U(HxY!*`vO$|xys#NEuo=D=d^0GpKT!tu9OG)sEHvMxT%7tRX*-M!H-${00B zX-%!d_;qAdmRou0?esm3X&`C9L?%kG>3)1= zrZ(@!xtRUoVl~=UO`ts@RZwA~!W5=3cLRdbrnT?#zt^fQxgBVy(Lw8<>z#vMbcyB*g z`z^f<`7ttj8e@~eS8Ho92*rM2N?VI($5-R8)}lLNrm{m2gfid(TS_Q{+o@g6M9lN* zP~Y~l-m7`_o!!#0I44S=dpdeIqcqU;xLBKKgFO)7=IOw}4t}|p*9g|`NU*&`+dtjM z#K%o1$lHI;8P%o_3;;!={b8$F{*NJ^7SL`a>tmqi9f1AxIINxntl4lPQO8wTTGc-E zb$ilgKa(!PW&YaDAt_oEc0{aBwqgkYr)c8u|4Oc%{CCkSWdiif+$Qg{~fX zWv&Zk!NvQU(HVud>q>5)SSI?_^;#g^L6m?x%31km~%gE zZ|t-w!c2KD0Q5qMI9T&mitZEeAgrN4CzGvG+iRG*dCL>|r<6eaezC8Q?40Z71G@2D zqA9fej^}k0cR_!0gc=G{c7x3vD88Id6TS{VX}(3Ln`VSxOW_Mtyk+h0CMU_fKW`8Ix`J81R)!%BouQf9`Y_t(bDoz~x& zu2wL9&5^gO19KeWd;vTVW(@=sR`aSnR_$VkKYIK@@?30gwxtb#K48eG_F&zY@~rAJ zpAH;t=!@QQ1Pr8s=)Olw{h*4cDq(>3@uG4`)=~4PlHY2cLVym)e!BdE4OkdSF}CTz zFb$7yfwi&-KgdB$mrLwG0&=A z8mKrC0Jfyy0l(5;>~gP_s$lOzW$}?a+IJ5;&3-e#5;WY5@ArZ4`@7biuoHI4fVNpM zs8c^pK(g)(c9vlzjObN!(?Qd)gt@&c>s=!tWRWTm`PhOFkzfnGMa5LX<9*=FLtv%) zn`GS&P&5vJSVjeU7?OZ)MUJdyqwN}`%AGyh1WWA>Kqh@ztJ2p^Zf|dmVmY_q5Tx%{ zu!`273m1^sAwH>i0lMB`1ghkl_4(W2oj?n)qIWdz1AS#ul-SULO~H*MeGmIzoQbcW=wbd_6pesjkq|iZ5T{FQVxQ%?`$8qC;S_r*Le|I~;7& zCH2V~c(KQk0K}=Ps^n<)X+(z){>b+%W8fFtCA~SM81C6$WEa5h=`6G*q zY_YmX_iNX+NDv1g>yHYFomxb143HBL|kBH;O` zERc~(lZJC`^h^-gxzcOe4qYYpi>UA4C#f(a7X+w~Q|20wXEnSLjrglD57vEoD|U6e zew=wO=7XY}H-;u=w`_hjUO$jut%vz0ZZ9L|`C+$BrCx%V_V?T`0s$;n*%dD^!qu-v zpCfQ0C$wb^z0SYk``uv0&Dnl7?ay0Yo^i%`ACQ5~?^momEYd#`c_unbG9$D=FJ=(r z644+6P9^0+9=7+U;fe}qc)m>;x-J+@I%sqqOF_pYW7`*U*6sNGfD$v&V1Wd8#LDA9E!RcYD9d0CpcwN zHX!`8b>nS)PBFzviI8)!d<5Lq`p0J6&y2?2?YN^ihHRc^xO%8f4y?zaT zR0StOJ3hI*mGr0A?e+X-7P<+JlD4waY8xTe1?9y794G4-4g1{}^wgH}Q5EkXM5hX9 zN293$Cfz|C=G6m80jKj!6#NfV?{3CNuqqu%Q41lB5HCKwd z^#}ab^k_4RdG{Jf@-rUgiwCb+4eSTWT|GP4=fpnM)@S+rDX#Ie0T8|Z;#yROdB|fmunFSfz zw|mXIT~%6M&nMpme6`n__i%gC8rJY#Z~u|V#K|;`xn%VjbqqSFrk(ze48!@;g13v0 zuWc_E=%VivW)U=@flIIOvUd4(loAcnWJpFF2CyOK?Q!=F@6OkD`=8r)jK>?v2oTEVsT2iA zcAsXU-4OFo?L_5fqycY5Bj8rSzPSXFRnSl<{4YqJxW~h^E*W1{y*dfJw`OL@C;aiyW(RTM zdj2EBtoXIRuJw;_vgxYmsP-SJN}n5j!lvXXsXAt#(8|n<0e~FWx|kR_)EaKlURHiY zdH+@6su-YiOOnM7szox9GWg?*kJ-!h_7%=8I6_{(4`7?{0FThQ%BAurCy%RUi8ty(Bz{@1uTuK@katF0cWO|g<~S;>Ah zl~roQnjIn*GQFxT`zh9i%5kAR`vnClMK0Y!H5mV%bRN?4j@+rck20EjC8kotEccU4t0LMw`vE$0xRjca&ca??@9JbYAce$_XZN zIC4lqF8pRkbl-d4Ldi&^aB=y1nV1QW1Q``ND)5sHPnYOCeib-+)xkguSnm=!T0zNC zm?%jHm7Wgm&=G~A)NP=kt(mGa67@KdyqoDzY3B|qm;QknvHMR|Q<{axfpgEtzE9=@ zFDQLlT)4dtI&5&eaWEAx@5^UMbIcbwrZ_#+nh-$3eiouxW~x{O^y}`F5dm)psz0mF z1wL1#O&)VMT*0FC=kPvD_14U-(@nBzy`Qr(bzl!l#Cdx3{t99?5(fAPZf>r%e%5s& zq6K1@m}AAenD6+ym5X}}D&7fP_nkKXK?mMUVDZ#QU60$$)_FtL;RZ|nuiu3 zz}(A0+}u1j4(@wL48d+i3e#LhJJG(Eo|M#B7bKtDv=cW&Vk>Wl{^KA~TIfx-@2(&;*aPw9o?riTOQ8tC4d%q5ODui zu6Rg)x_;RM!4eI8F%4%qW0t1h3gsF%ohV_@V=nE){3UNDBauI>sFss&P4h3dABpC9 zKd1IuA;Q#EuXcYH0p}4SPUZBQoV z;60Vdubw%qf~w3t4imO9_1a1Og%K*f6#}+!yOK^gFy{Fw@U(XlGI-0cD#Lx>P&S`_ zmBJCpSbgK{xkC};b2-uSMO}*XwEeOn_4p5eNfoVmp{vF8fBpGZlLQRm$~i9_j*JoH zC;NZ~$#w)&ap{HhWz*P;M4Fue(#eOL`F*?ebnNVm#t%cy;Hb;*pm}cM(pAzsLN#Ar zuV$Gz)7(YXJUIH^fnlnrLgVm;$9;V7Z2zv5qnR@+!?JvHx1*V4DbXGYLfEdj!~Hix zIug#zb{A#2$l}xAfW`yiqlIoj~lw{!@f=hmvksuoarrT zIW6O@IDn%*LdTw+EEyeXk8t|#sNT0oz2N7P7x?g=2(`J~I~P1EEUGk20n0!2DoH@9 zmN`l(Wqt`?X_N$jB;h?N;|s} za(@!nU^JIM%%`l%OK1SVhjvuh?X6$hsc*=guv5><&QliT=Sw56SZK03$M?Zavp4gT zk@&!%24!Bh>c- z_=)bG{!tfWTpZEZ;Ionmv6;hR4y*K~b}8wQwX4@MVWX?X+$0OHi+KrwhfECVEDzx> z0@|C@>K@*%OyyJ*era)kZlSToQwe=z!+O?x7W%Gp6dE)+uux#Q8e|c_7ApH)9CRp- zGov$HA}ASezdUHp%1OB2Q%r3vI1WK!hD*yYPgo3yK7S0X245LWTP`UB+;SR<)m^^t z0vL$_v&IIj*kRGTH@WmQUFL``8C}Oh4zd|B{{}3QdHexP>Zm6SC{#;kSFPj~c_ zeysyd@A10xzm<0Bo8~d*qi^l~pvd17d!^?gAR7Bo{N#6iA{WKg!Hp&w>f-{+EdgI3 zX`R65?B&vWOPRWbny_Kv3&WTYgcFO0#8_(fMNui~=FKfz=>D?q$4fp@#v_~UT2Zo# z$>{TD<1GL_zrk!uepxJSz2oxftzd;D8J1-_YR6v%wuK#d_h%POus8)he*yE%GYnQ! zjZ@}cc(finrlX2epKWc%hYaQV9(ytm@j^bOnUbu0^fC1t+w~wZolbg5>%MM?PFgHT zJs)u%)Kp#j@U^o1_SO1WEyZr7`_jW1HEn$b%Gcd{@4((fQ}o~5>MfK~PT412moY33 zCdVr_N_Di^=48Xzq-`at`SZK=mir%pf)cpBQyz9Q38+Mp^Tk;7J#LPUZ z5?`QtRqcuU!OT^j16KT5UX;t-r9PS#9FjX8gSnrAi6cf#fD9LkkCdQ!! z`nD3d_s&z_+1YDEvQVQv!6{C=h2Pa`4%uDYyTr}<; z<%=7-&s7gR36|S!`Zjx&u*@iG<@hFUQ{Nnc2k#TTb6_|_xG4Q=_(u8JRLCRaQf0h# zUHtvl>jY8I3lt>Y=wmCrHT~t1fIs*wzkk-)^f@w`wEfUO7MAI$(%%2>sl{muD|{H4K8HpWlGegK*2KF5uLzh)!OoWn=6kcL&`+pw!<(`f^7 zI2_4zY8#{GvfA?ulOQ_v`w&NdQsTi~dpd@|BPr?QN7`B!dar_hmCsOy{KiJTm3lS? z6tE>~J?LM#M($i%IK%*heL9j&^0Kkl{y12@!&&xyT)o>IS`b7(XpAXT4pRX3P2pbQ zlq(zipt|I~b4LjcF55bZ)N1*=&0+0c8}~HVP{2d1Hr@S%KEMo=?Ieh!I=t@(7Jrlg z-H-u!vo1F=K<;#3F4yii`LWfr0DN((8Ijc@g7ks1XR7nU49BXf?7z#6PkrTT{wqd( z3GbrvTDJ`7JM?@|5?#Z1s8qTC-V1JFm{Rq0FP3gS(bgayD8PNE*p;7McHCIFxdcJq z`a-%m!8ny6Vl^dxReY~&0K}{T7z-yv-DCvfmORFS5;$k3V_F&zj&6(;9iqy9VSUlMsn@@Ag-qjojO%`Z~ z4lN`v@TA;`jwIyU`w@o^kla#^t>qAW-H6BV^eEsYFQjmbMLf*(;X-V@7*{yi=`#=( z6Uhg+8{{8k+FGySB$eOIO(kIRF>$;ryAwmy17e~KOS`mvLGONR6L%{nzp@Isvr!HM z4~Yj#`|9GdPXyL_bV=HlA?Uxl1{Pv=L!A}kHVU1CN`yCzYYdB=W$Wu4Z|@Npa6c5d zC4*rHQavxCp+-+SVF<)Bl}4h@s^-6-k)du!s2PSjrnIFC^_qs zsJ`y_FNQubarc!gT5y#mLSslXzl9Ji#awY897ht=?@Y%0H}zJWv$;#6Q}w#^YnP4x zQgv*M_;Xegx(^Q_`tXUyDLr8v(c*~&p?Ch9vGc6b#CVKddwj)WoS4&247e)>A$7xI-QP7H_||9Ilwi8`|{}77^md@S`f*sJJgYgsCz>} z%8gs$>7}1DS3bqK+?k7`!I7;VK1l-$_ZhF1mCPAHXygdY8{9u=W^5c5;WXH2`e zXaQ1i%faS&=wJ4#njoj~)LPx?Tk7sqoECMb5uV;t8>kIQLDV1V;GPnnbaL4OPwO?% zvy~SPZWn+(@`V+~Jy7La6iV$g_af5to)k!CwVZB9#}mH?oz(Tj*XI?*5l!S`4A&T6 z|9M~CO5(TKD=FdIWcyT*JHDmD<{wP|3hutRZ(%ojy8b*mY)-L2wUW%t;kl15IhYaTxSPlGg&g-W_Rn5ZTPF{9zS`@o zjIU7t`q1p~GZ=Q;#7Iuu-#fF#D4CZCHnyLxVf54g4)Vdfp!%Ffa*_aA^KI){1_uwASo6>k>cF;YO`YRJ zjN()jZwyx{7x7x4cK!u^?L#)o%GnxP223B#tWNXyfPRzhSL$Mb?+Et1NI$=X9>2)M ziE@(flBC!Q$S0g1Zp7LzGiYo$HwP5EvyPEEX7Q=Ab7b3XM&p8^j`?S?phRFCS$tKw z%2U63hR*kQv>l{;#6=C{A@M*G|R z_(Iw;abJ|TEcjK2ksq+0sBt2>2LjL5mv8@Q(P@#G@_UnoF*Nr4HtrdT)Dj5MY?6fitJhz28=y0+9^dk;tO5fExSkC3t zwv*8BQ*45BW)rDMxv#OsuP4HVpJ1#Gl39{f2lot_2+ZM8Zl6`&;@DKa*D%elUFHS) zf+BuK4ik}>y?Yokw%EM3y)>`@-;HGbNXf!&YR(l;jSS;Y3oRT@6vpzVS96x0f)K@f?!oVGUdH}*6Df#R zDO8Bb*3+1uOE#rY& z{M~eadTj)i{(#c4^i8~P^uYgm0nlMv-%?L!`gC>7Y{Azl0P^xpE31P4l+{zo-MF9H zKg)+s))UqfG`W`ALuVWx6^R+9Q8%@0Jn+68?ms5Nd@f(#Hy}AHn~obhY9^nJZ%W>I zU8i)0oSUw7dj{#fK@5pm&i_0(!G0lpyNePEz3drTqC9E*k!{HVK3pU$w?~g#o!A?z zb7UwSArg{z7zLL)A*8fb&z%zra~pkk4apN`JyR#*0_UKv%v+Gd4_~B@u*-wNd62Fimo=U8 z@HYJDMlerH&|e+YZAO(hnds}jXE5mtozzb~JdYdWjG6d8XC_t%?~t=;?E$F^Y}Iv;^?E~!e`k}MCTICx%6hBiv&3Gg~2u=Dz`ZwVKDE2tot zxCK+FlM91WWXp1)Z8XvlT9I`|HaghLx~N z+7K?|UYj%=oQTRn_rksa3UlEt;jBB~^C_GyZN(<~M1Z#&gYS~^MXjbXrWmIrHKLq_ z1%}6daV>>HzTbc-{(aq}vnVPCr2ZH+3q;t<1h4jNaE2e=EBY=FXm364WKxDPllzHg$&vrymJp@r*wGrb}|8ROAbpl zlGXUeiR>`GrF5C;_R_0O@8NVrm%vMUEC}L{xs;N$9#Ve$%=dJzfr1`ORmz|Gr7GF2R(U1m7Y3R$glGT9v^Z3zGP=U7p(MJLx)I+@sp9 zb6JF-Kan5U-so_?m@?i-jwh?@>vT|eO++~1L_P-FI8mZLT|H5A+;RdcCa=8oqWx{0 zXI=QW2D9s6efbLY*s zuC#yb!4-PCKJ@LtM!f{q-ngVZe)jJ3?yC@;r*Y?A&HQOggE(m6zeInSy8Rx8oHNgD zpUv4#hWXHvg=c=`hS0;Fpw~dlac&Rz!Bl7W*{biIrj4uN8C{fssv!8@BpmJiQ1O4I z<}2gF-n`>YvH=Z0vthlbajjf+0qZ9=lzNXr;!z3a({T6ZPR(z!DOuIEFr747=64Q1 z&4AlR{;)%9s0Zo(lE6%L%65!%LS;Um;jPj3glO_K8CmK|`aj~R)~am-#k-!og8xr@ zU-{Km^MxBI6sX~)xR>JY?i46eoFK*By;z`7tY~p}m*Vd3?q0mO69VL(zWv?*;C{So zoi95pD|=@4nVDxlTO#YW?cqGU00UrH)M$99NzXHp%AT*43llM6ru`z=_TNv3mxS8D1xIBbd{r(-o{}6P zAXn~#Vj8%)>?F9Ig+I{x&)OridMJETsA~O5*bsGr(^@juCV)m9C{;Ak(`wUI))85q zbWtJc){$7Tl1AP(#{Sz7#M5wjdTiniIb39QW)W;XBCQu*p2jAndQxmj;Pf|m9$o}IAgt3-yIDsve zV0f8fXcIkHF1xV z*FWcT%37AGM*wdE-}v}{!6o(Wc=B$+d9Pf?I@cl1TP%Cy_Z#GX#VbQAtY9zUU;WxN z#+#7DYQEqvJ}R=fzxvx2gysm7#<4a44_k5}Hf52m_J{}fMJ7?P9_}KK0To8Wgm(e$_ zb%Sf3TeBXkb7$v!o}cb0bg8VZ`O(*XHF{N?M9uXFU#rerLFgqwaL@jE2wk}vO)s5c zP|HSb-)|a>G+-J6_F2dFJ6!4w`2}dApmoM+g^2*m8Pxo+p$fgvqZA+%2j0+95pYDK0o?QBt+M_ua3E1y3Vu7N+sm; zuw$4lvCXEQ0Zmj~USL`h&?@Jmzs@!%arH5xwnEsP9tMP0-(afs_<4ax1vT{Dl~h|C zw`TAc(J3Z`hr*wM_%GSH)RhAQDzP$)wlOMQev$_2sP40LKNGPyUWm@9v>=mA?I4ZM zIU>GrH$MDO#Ql=CEbs`I39*J9xD%@IVh6hz03!b_St^xSfBzw{nvr9{vF(x-+Z z)K+ULxL4a#got&8Ns9Fdt&&($K~f2$yQ3fg1&G8vcjel%+Tl@YLW1@-Zc|qD&M9EJ zcH$Az^rjS zuTD(H_65t^r1m(g2TCZD^O6@v>yd;3O1rH7>Zps{| zFliy_>frK>8X{z30^sw81_Lcu)0 zGGkMcnSU8TvFc@0enk8rVSKnhA#ic#QmyHwh{WeeQ!|6~;`_tQw|7F~d>!b?K|U$N=lM0+X=Q`TUp`qw1IF&Ofi@q#@4HwsxEvsb)_7 zc3%}29gNYVB2R+LQrtl@v`_-Tu)*&E7N2X`$Suy>vp6_*DlydSSSC4JQ|1He-8D<% zQ!=(L_kr?AZ$Jy4LU4w)Y#)3|UxC}VoKq7WSdwO}=O+FAVw%#f=-v{GtX12!i~=AW zG3e`*XHX;V(>%>H^|-hGWKFggIUn4yL&Yxsb^}O&2!0)BGrKye%1DSW5wB@qsO7!- zq@wsBU(+YH^&j6)uKf%YE$02Ty1KyHw-aDQ({}pK!+djH#MEietxr}+wHg#H+~O7xJs@O$rEmVpo(8LQ_?xpXRKr$=~UM-|-Y`#;Y1JquwG z4p?{@mhhJ}frp-o^SbtNNpQ>4dA2{yt6%_8X*BXkY+1iD{3Kz74H~&~!Ihw5$Ki{9 zYqXWs^rr?1*Vi7Z7W7XYWF(+_O4xlOQduKDVZWc4T3{l|RG+$Q@0>g_{=ia0VFwmj zpxzjeqGBV;$Wmtsq+=SI%m@AImoM%fj!+SUFyki;vi5L_uw)|LQtBLGdM5B}xsEb% zN02_sdC>p8fBBAI@g>$49HEXd86wB7j1h&MNesP}4-cAC)9HgJhFDLiJ2P)zz@&#R%Un+buGf#(;j=gf4v;nT0 zOA(elrFg*5B6Pxy$M^&*$~S8>$hk2~65GW_MK4Vp{K&sVTI>J$VS|1I!q63V;B{3) z(@F^lHsgN3CD&r>lNQ~Gf*=gG)k9oL6*e#`Y|oU?BW}}K)Vg~U4UjKxv$SJje?#aw z!qqGf0!`jEfyZ7P_V2zD#}Zts)VIoWrulgzVJvddm7wy`q@(U}<^6uydi0opCm_I7 zAPw+Noem$TCIJaW8a#o71p*D#vh5fKV;!Vjec8t(APSMr=j#8LEra4Hl9m z-Aay|oTn7q46!(#8Q85~UD1Oi zH8^qlfx^C+SK7M;XRY@>SmzOaw$a-5K7OLT$i*E!HsX)kP(E5MD$B3>wKLGfOME2v zvcrZ?FC@~c3Z@NxS&i%P8uw*(`Lt5tXbUR!?fWxCFCGxrKCi(~u1OI);=-G>Zz8+` zbpRsBB}4G+T+A9)UD;m)9xHU!D{;Xo9H^Td9ZK(PebWi7i=)Y8hd2N+j{V>_6YI=# zlfrv}kX|e!m@MYN=t?3=ZYZhKq3dHbF>t-Dc1t{#X#;J8Ol+;nfwpxD8!aE`6i*Tv z3A5-l=;`#Le)r7D`#e7~kF2P?1Wm{l91j9{5YI_pJw?Yz>K&MGt?egklaCXI$rD>U z1$^RYFcLf|*|cLtg&eh0%W(}4t|K=U=9*YhQe_vlS38F*N$p`F&1iofK38?emF}sNPU6w zCL*_!*v?mw$Uxe8IFaF`0%{N4>?ZUpZjRq6uzTH~epXk-UuXIURe80%m_*kP7tK?C8Wt-@~bdM7W0<;x+rHsH9VQ?JoO# zHwXn%?;KgT7qKpYf#aD-N&rR+Z^8Jb^9LZY4`$0aa<^I^rT&y^^GT+Ux%8sEj>R+YyA%(1LB;7e7hA;3Fc=Kxbv4#?;(!pvxptsp1~+38q(Yf7&mj@ z{?!bBcrgFM_+NJ-JePyT?pu?zbN;{;C`R7L8=eNwft*R=9EClEjY0+995?1_=~J}+ zp-nb`RzlUJ>IlA+Ke?N`1!2BD@Cay(iL8~HQy`!+gv~fQ4;&HJAw%qL{0&R;<6LZr zCsn#4ff%R0o%3d4q7zpr3$>F)hQ&L>f@=WgsCy^;BJa_Os`vYiZQsl`NFdH zLhQoa)71n<(H;4XLFN}nP8j;s7?Jfm7VA5Tj$a6{%(cLcu-fY9uQbmjqnP@J_(#pE zN!bP21qIoyf$lRDC3ikQ)$s8RewlrB%Mbdbu5O|(W&Gq}(u22t3!^A`!o@$Ae8Sdp z`~AlyIbrM2GAs}4=M~ixojNGpENbY7V2mw|YFmwUcKP5W<;Lel^{OlQ z?{|Ib)BL+q!?&`;{FcY=`cIS|J3T2=4FsLl>2F4`Kem3oK0jUext#O8V0+-UU58f0 zO!yvWjrx1D-L%A@iGM;{7D4^lu=y?)5wTv`fV;LK{;usSZn9<@fTr!Jv z(Ra-6OM&*7mC*BuF{gK5@Qo1_Qq9nt3cFEy&i_$&O!M&I%XgWny((~Sa@bgzr^0Y& z^=ocDcoKj3mQ)*ViMl?B-s*@}C^39nh@=S&kJ?#cZh4v=CCdE{O(TU5mYCtf=$@6z0c+r);>7FxnUQoxE z&xG--&F+-u=1YER@i_#M^qTjlY{rGfsTyOi> zFH7EXp2OeHi2Rr$d)%_HPVxyCEsnL~SZYvj8yS-8m=u|Rk4A19i*+hl+Ghz%Y&;!} z$-#RnH2#Lw1r<3dD!jhs)Bf`gY@Ov6CevL_`GwBG`7ms{{Yi6%IK%Ti1Ptb@t2ad% zx4iRI>%D5NUvE)6uiT&a<#}okj&0wgRTTHKj?5MJovkhy8Do8pFkvsy=Sjz(a_knP zTI;K4@lnmYtWJNn^n=9_*9y0jjeDRZYNfOajk3_75#yDwOvC1)jn8*l{l_q2ADiNp zy}fW@Lb%JvoyCjx0^eD$HUSqMR-QTwRu?{1Ive_v^tXB1FC8rH)_L@cQ#Ej&9CJ3~t7?}t6!mo9d4ipg}*Txuu3&c=AX z+I~R-ZoOw@^z+!}C=aCW(X>nFs84Ba!9P7;wp*jKlH9yr-tv_Qejp{uyOlFN+Pvao zl`J8x%Q0r3LhLvg1^NaRw9T|c3{e?|wW1e}3N+61zZx04iRGa@u*L7-y(-i&!x=@{ zQs^1(n#x&<)XblrrjYqgwGIobw_SC(^m^)yJgLY3y?r#>`HA~c)-o(EJ%%N2>O&Qx z$lSuT7fV?3pS+QnmrK8_``-x`mK3&m2Qqz_6&$D@+%;H5>67hWS9>9Yo3<%<7-=Hz z%ncF}XWWnGapSg^kRXkZnYABl>a z?gMRf$BDK0{z7Bm&7hc8vM(_D&XKO|NGFWu#Rzjr%3@-Bm zWU+XAcg-+X(VWJ(S5r`zWEAGDOOiOX7EsD4f9A(|%Jn5qG=OAY$lzdXzjcmnAVx5T zi0+xze$(B#wgT3Dq|e2!o3Hi@hIXzeLDCd`eXZ!Xr;NRLHI0cPRLCI3(sr7Z2+CH2 zp<$HnWa1UF09Bj*=7cs+3%NKsb4@nZkBVE-+7!6wzjgPP=t4a(#Wd@jevr(3y$62W z5k_OT_Vuu8Z|8d1%|`2;9pVjDQAhle**dN&=k_7kqel^W}!Q)WF7>Y5w4fCWCe*>pi&Er@-n zCWUV_pSHZaC#*kxp#Qks6=jRmTzb4--5n?*tCu9v-w-W-l#bKWlo8nGE#K14_iPsp+7gjn7 zJ}c=<94xAJ&Scstlf#v;F8I~DB?i+4ldt}ye=BY*yAX-S1Swo@Chjj?IUD^L3Ms3z zn>*TIBF}AH+M;7wV`8p zIH=+kqo;o6xhx+Is)C*^=heK<9@R>F?o^qeEGF)0fqkm$%Wz1khj11+XhJk2$W~39 zg|%HwRQ>ul?~T>S#7mI%!==?9uf_7pRp)sfyO23vYjH2b$XvPdDD^aXFBcs4pYOQE zyeK+IKBZyNS^57i`P~ppNNAlUEMLn@1T&pdjjkON3e*P4h*q7{?df3B#-w>kX%SI6G_xXT;!7tLMcACXds7ha7HMbZlW3Yv~?-Rr8@ zV`u@atFl%t6qG00otX=&~I$)xZsQ0tm<+_Ld^Dx2NLYu&o!u%h21w7?fLbv4g zyD$>W{#U(LpvAv;%Uos1@>b;K#0$`5lkldq_0~vxKlUz#)0`MQD1b2fLtpm&Zadzk z@zLBmx6`q2Md)-Eop=_@$S`Zr!Mnog*7G)VfqPT6_i6#p(egkT=f7jW=_9LU^cnch zKS*X4+dc6!L+5nh{SxmgRJ{%Fc*sZxyeX^{&ZfRk*UI zBDN>4VACN$2s^zJ$(cTBXS{ifM!n>_uYuLJq@(Li5t(X7WSMvjZG9$;)`;%6F#~&)suwwJu|VgX?O@$E%q=+g8aZOfKFKM zCuGDW3-0lB@4_;a`cY3EiMP;?3F()7-r>`nYq7^Q6x|EZ^$Hhw+sW3P2K**=qdkwG z{?2$kHbJ4X8$aY|_)B_PIv}(#m5B~TdC|KI*iUTzK~sz|AuPCUANY$zU5yWVkOU)w zZZwk2ms1?e|2*b($A2|7pPagv+q90j?5y?BQW1F#c}e1KRBJS{QZelc)fr6T`t|_1 zcDH^+22t~_WsDClVdI3HxjV)hjV)NGb8x5VGZs|*@unhAd5~J7k~eSdoLR7O|FXN} z^T}l`;ZEb1-TSTN{*7i>)1I~tFTUITMKnF@39RSN3v`SO`ME%Z{V8aPZpD(iNqZ~6 zA_OXFT9wZ-h%zrmZd=;u7a5$XUoqEm_LayYwBL*8@pZ-V_c6WBAN+#(RjG)wr)ppFXX1FG9;@K8E;@nXtRlvlr^No=3TjS};`Yw!w#x*trYidqav6bq8vEPbNyH(Pt9dlbCUQ2&5Obk>xdN*#{e~ZSqa`Ac(brzG$g@um=Lt&6 ztiVkyUg>e7r4!NEzv~%En-xMQk2W4;Uv9gKYlAY`k{6y6DAUE!$2LA3qnQ1LRvbDNdEIFpm*S~L27GE)bWVQGzWZ7FSZ6Wzh z-)CLyt6Zt(SrRRc#EnOu-GjZFX$6qXG!&n7%xhio!!KQY1gBF z-L+e(>3zFeRp5TzE5e&mDusvGyTm_Dtn&ve>6nbLr3`gi)=z#M({dmsHnYh0wG16t zGOphUYC72F9PP|~(msX>Z_H~ruXFmIShFNFpArNns>>Z1j0KTKG6)ty1M-?`nwdM> zqP1v74$i)zS^G$d&?7*a*6-!PR<8EnFt9@PEwa7shR`fJ@tfRsbn9HNHLjDi0)pco zEef3YRq5s1E)r(y(%+c7Mn}jmajSf5bM$lggao24xG}GK4xI{u)K0i%*t5Utce}We z$^ZUN;iu5ms>cv;Y^n6mnErfsIxZaNLX&fn{loQeVP(S~MyYy9L=_64WK9wm?<3*cS55=_!{xP=&Dit@x-pi&anAq`n7b`=AFjXSReHoe;+w` ze>pGahK`iRNvOuo*hkAl7}rUOTXZZ)y^5{R6Q}zfpIa<0C?<-dJ?RLY_`DE_>b0%l zv6*%dVb-Ck)?WZr=u4$ELpsjWdbogpa?zO$(^xYq(Ww1kC`qeD8}WV?FNet~)6j|f zuixPpnP20fpJ!;Ob~c!N2oFFNZ(i*mUkv&F&7r`I9?NCmz``q<8rv+TTpo9KOisfX zi;#36pK=AZ$@Ot%rRMGZ(yHsrY(ZPsG7^_-B#{6g7d7V5&L774SX+G`b(WcxJXxA0 zERV9j?5p{z*3Afh3S*61Zpw*U$8F!jE&WYNcD=N8VaDc?pWcAgI8tm zB};=TA-E>ZHDg1lAT6-h0E&9MZt}8u-ZKQtY6kGptRrRQbR9c&`N^XOQ<{jE@BYrV|Hb37Ud@Rw4MS-OaZu$nkI^nT zZcT*N=;jsa5Z$p+?d9&CyXZ>@c7dmp8V8BqExXLZn+Op2s8QNRt_0l%pIPDDs$}vZ zkM^MQ+WE^*jO-OOBC`)EZ&|^-?f#xkE8+5rWhU`Qjl1OHM^_mWK``>xSqRPr(CG^kzQ27%tVljX$jcw>5EVVpq zgRl$IL3$ef$S=ulI8_cxw*w=tACwWbup9@1rX>wS2W*C}zLF3eGXC6k{TYKOQgfup zljaUTbs1x>(!fO^D^LBw-+H+IUUg25o##j1a|HHfXoQ0jOpvpsvEyYobbw?~;%OMp zOJ27CgDf4*`;UQftk~{)Q`2VzOp2JO?LKdRYed#^3cZE)Ri*t~-i|1H0XagSGcU=)miKbw8{ zdZMT*2ak_Iubbf)4`Yb>V?8dUa(^M~trgcLzr#hhHrZKGmT%m`lgU=ka=#k_ihAoi zf3Q$x+#T|kzqLD2X4Z(!m7ssrtE8B8d-=vb`AY&6T0O8(;kmAJd}oT`G;*-!rNKc$ z=j-o@QL!?aD2=U!Q>-)Zjvkd3=tDV$WW;{ucpL7-vu^CTzbvGly#BD zAX|Af(<50X)RHX+IB&gI~;j^ehC|Q|)<~InpkqzvQLW?9gI4 zZ#YOto}_4>vcmZht+P%Ahequh4|f!;CfnTBE!O=&XDFwE>bfsM_|*le5y#K9B6prZ z7kSrF3`q^*idz*$I7qVa`1IHDM@jhw>a%T8mCQL;D-AY-RcS_7nd%{w6*o$*fjvih z^{koRJ4mv4ppnL5e%D-Ufzl-uk)6#>e_5oF@Ob_{^`F{^pgdo>bc#NMQ6s0kLqck| z!e3e5f+^(j;cnfyjsg|U|Cj`1-=l~j4^NJ~mEuhFfjAD2M&Q8iSAeZ0o3FwcsVN~U zOBk6nm5Q3humn}dcX__6Cf^uu0Z4-k-ABN{CC0<;uNBg)gKYk2e9CjIi@&$ z)wx8IbA#T{NzdUFKsV@a2pT{A7`|bc7m{*2{I-l5jLVaNf7u3QM<8PqqP)Lgq%d8- z@zZxveyONB{9uI{SnpicX(=H*|uDEK^JnwATFOS1Q~GgIAdq zI~a~$GhrLJH)GkWGC%mnxb@;(Dn>+W0 zXe{b4)p;brBD9lv?syWxB-=jG#~{xgo)Jt>fVwH zpR2m0fAYXc&%FFWs5}VDL~AP+sjL@eevIcp+LK##M44V%K1@TvQ~%VtqMex_vXbIJ zHN3Z zMw9|?$#A&hsOyPY)eVShk}Wk-(WJf_I+P z&Cp1OF@s4}zA}4J#)_{ot5VBR2RQV6;is45Ob|-Pz5xUutw?!_>Ey1)jLd$)T);q zu|>d|hQYqmh6}XEj8hT)JBP4y-sIEaV{Ly+9?-NP`dK(=+ zpBaYjZdss^TXvbeK42Podm)!^pffav5j_QnEvI62xaoLAlrBg1bQ?GIufw~GT=$7C zO}iuxH^Q&MEZ>>fY5s#k&t0jm*x=b0zFhP0#24MSCFu?iie>o72$||a6g#gOleOw* z=C|A?{sjx9P+1>IUf%dBhyW%<>9h3~ZrteOT}$w#s7U>yV%mWnl*jKY6=~IeT2Gj5 z1iC94RgWcn#155Imfd(r z<)VOvEf=g2{YyBynY;xbv8}hbry`n6-Ph|Atyz%*BQyD$6VWJ-(0VbW)H3att*f}& zjlcNg;g7lCbQ@I2MMGAC22#2j@=+~nR$D6ERr^i91dY$5XB-pyQR=FonBcEx*d*A$ zcJGBKx~`7R%lB8(9klI-bNVex28fVEB=05`Um^nelb;lg*4puM4d+O-@F`S#TF$V! zM94;vM&$PZa_VqgQa*P3DBAg1*6&gzo+lh~5JIL5MfTD2Rf~rH3!8MjeTVXsPa#}0 zgpn63Vn=z{na$VeTIG++dAA>0^8h~g!jDr}|2ppEmG)CA2nqCF{0xbTQQPU62gSz> zjWHROmf@i3_CIlg3kZG_*inByF>4Glf%TUVZFI{q5>yc9B^GbR$bTo8XWhfTvt1iK z0XjK|g8L_iJvGulHaGq0bAoF5S+p}s7FNY?x{nd)-sB)J$cvvRXzySyrzs`G=_}8e zje0GFYW_T?Bie&d^CVh=b zsQsiD%Zam%#|@pRs63D_;YIz0$!(qUMWz0&F^0H6t@ISeR(i+P8Y&XxR^Fge2Qk9K zkn!pS^JDW68>Z9AvN*mDoyuDQXGFho)I=aq)+ZH4qlO?k+}SXVCR+Yy#UG@^$&PtF zJW%gre!}rowR~22bsGO}>05^!>H>ou;|(VU#UY)cgvn4_v;}2;Va323KA02;2)Nd; z=rAEz*^-GPxwuea$ia7}q=Yf<=n(!zSR_j{KrK31S3o<`kDMmhu57=WArGO#;bxop670P}_UgK_0Wp#Y z#Du?pU$iVN&#SQ?<>Et6-r$TMXo5=F9vevngjB51Vd_!0K=~mnE@7Zve3W zDNoumv+Z2?bE5E^0MG=FhdnT4U|5nJ3$|W8b6{yIa($*_Fqo(wMcn~GqZVs(lLrGt z(iK4acU-8P2tg#(J8Q@Qq#Ri4g5&Cfy(#p(sApJ%cz94D7&LfUejro`gcQjfb}!IUtj{?{O@V_`!=NGfBh#xAU8;4pc+MTL_pzx z&n+OYu#2PrlQ1UIK}e7rS_z(~zU-={n*!TZFFb51F0>hyI{ZE}eAa@9|U;W?F9HWVV%rnH0 z{wW6`o&a)#*>{rvjVAN&WRF#XLH|?N@&70P|1JOj=C$vDqhzldLdBc&0OB45`Y5R& KQ7-ny|Nj8YQ~@^t diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_image_256.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_image_256.png deleted file mode 100644 index ac455ada3900b28af2246884756e0b801977ac06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1671 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|GzJDXKTj9OkczmsR~UI295|Q_ zKj_c$zjsWO{q>2mygV63CeKDwhLiVN)jqKDG`K4Wh#4?E{N$>h!0?BmaYF+m$8>!w zM&<|X94)Lo4m)Zta!N4RJ1FQRFdTZ??RS9T2P0D=BeQ^Bd diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_test.hjson b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_test.hjson deleted file mode 100644 index 633b8658ad849..0000000000000 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_test.hjson +++ /dev/null @@ -1,20 +0,0 @@ -# This graph creates a single rectangle for the whole graph on top of a map -# Note that the actual map tiles are not loaded -{ - $schema: https://vega.github.io/schema/vega/v5.json - config: { - kibana: {type: "map", mapStyle: false} - } - marks: [ - { - type: rect - encode: { - enter: { - fill: {value: "#0f0"} - width: {signal: "width"} - height: {signal: "height"} - } - } - } - ] -} diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_tooltip_test.hjson b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_tooltip_test.hjson deleted file mode 100644 index 77465c8b3f007..0000000000000 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_tooltip_test.hjson +++ /dev/null @@ -1,44 +0,0 @@ -# This graph creates a single rectangle for the whole graph, -# backed by a datum with two fields - fld1 & fld2 -# On mouse over, with 0 delay, it should show tooltip -{ - config: { - kibana: { - tooltips: { - // always center on the mark, not mouse x,y - centerOnMark: false - position: top - padding: 20 - } - } - } - data: [ - { - name: table - values: [ - { - title: This is a long title - fieldA: value of fld1 - fld2: 42 - } - ] - } - ] - $schema: https://vega.github.io/schema/vega/v5.json - marks: [ - { - from: {data: "table"} - type: rect - encode: { - enter: { - fill: {value: "#060"} - x: {signal: "0"} - y: {signal: "0"} - width: {signal: "width"} - height: {signal: "height"} - tooltip: {signal: "datum || null"} - } - } - } - ] -} diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js deleted file mode 100644 index 30e7587707d2e..0000000000000 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js +++ /dev/null @@ -1,362 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import Bluebird from 'bluebird'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import $ from 'jquery'; - -import 'leaflet/dist/leaflet.js'; -import 'leaflet-vega'; -// Will be replaced with new path when tests are moved -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { createVegaVisualization } from '../../../../../../plugins/vis_type_vega/public/vega_visualization'; -import { ImageComparator } from 'test_utils/image_comparator'; - -import vegaliteGraph from '!!raw-loader!./vegalite_graph.hjson'; -import vegaliteImage256 from './vegalite_image_256.png'; -import vegaliteImage512 from './vegalite_image_512.png'; - -import vegaGraph from '!!raw-loader!./vega_graph.hjson'; -import vegaImage512 from './vega_image_512.png'; - -import vegaTooltipGraph from '!!raw-loader!./vega_tooltip_test.hjson'; - -import vegaMapGraph from '!!raw-loader!./vega_map_test.hjson'; -import vegaMapImage256 from './vega_map_image_256.png'; -// Will be replaced with new path when tests are moved -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { VegaParser } from '../../../../../../plugins/vis_type_vega/public/data_model/vega_parser'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SearchAPI } from '../../../../../../plugins/vis_type_vega/public/data_model/search_api'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { createVegaTypeDefinition } from '../../../../../../plugins/vis_type_vega/public/vega_type'; -// TODO This is an integration test and thus requires a running platform. When moving to the new platform, -// this test has to be migrated to the newly created integration test environment. -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { npStart } from 'ui/new_platform'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { BaseVisType } from '../../../../../../plugins/visualizations/public/vis_types/base_vis_type'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ExprVis } from '../../../../../../plugins/visualizations/public/expressions/vis'; - -import { - setInjectedVars, - setData, - setSavedObjects, - setNotifications, - setKibanaMapFactory, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../plugins/vis_type_vega/public/services'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ServiceSettings } from '../../../../../../plugins/maps_legacy/public/map/service_settings'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { KibanaMap } from '../../../../../../plugins/maps_legacy/public/map/kibana_map'; - -const THRESHOLD = 0.1; -const PIXEL_DIFF = 30; - -describe('VegaVisualizations', () => { - let domNode; - let VegaVisualization; - let vis; - let imageComparator; - let vegaVisualizationDependencies; - let vegaVisType; - - setKibanaMapFactory((...args) => new KibanaMap(...args)); - setInjectedVars({ - emsTileLayerId: {}, - enableExternalUrls: true, - esShardTimeout: 10000, - }); - setData(npStart.plugins.data); - setSavedObjects(npStart.core.savedObjects); - setNotifications(npStart.core.notifications); - - const mockMapConfig = { - includeElasticMapsService: true, - proxyElasticMapsServiceInMaps: false, - tilemap: { - deprecated: { - config: { - options: { - attribution: '', - }, - }, - }, - options: { - attribution: '', - minZoom: 0, - maxZoom: 10, - }, - }, - regionmap: { - includeElasticMapsService: true, - layers: [], - }, - manifestServiceUrl: '', - emsFileApiUrl: 'https://vector.maps.elastic.co', - emsTileApiUrl: 'https://tiles.maps.elastic.co', - emsLandingPageUrl: 'https://maps.elastic.co/v7.7', - emsFontLibraryUrl: 'https://tiles.maps.elastic.co/fonts/{fontstack}/{range}.pbf', - emsTileLayerId: { - bright: 'road_map', - desaturated: 'road_map_desaturated', - dark: 'dark_map', - }, - }; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(() => { - const serviceSettings = new ServiceSettings(mockMapConfig, mockMapConfig.tilemap); - vegaVisualizationDependencies = { - serviceSettings, - core: { - uiSettings: npStart.core.uiSettings, - }, - plugins: { - data: { - query: { - timefilter: { - timefilter: {}, - }, - }, - }, - }, - }; - - vegaVisType = new BaseVisType(createVegaTypeDefinition(vegaVisualizationDependencies)); - VegaVisualization = createVegaVisualization(vegaVisualizationDependencies); - }) - ); - - describe('VegaVisualization - basics', () => { - beforeEach(async function () { - setupDOM('512px', '512px'); - imageComparator = new ImageComparator(); - - vis = new ExprVis({ - type: vegaVisType, - }); - }); - - afterEach(function () { - teardownDOM(); - imageComparator.destroy(); - }); - - it('should show vegalite graph and update on resize (may fail in dev env)', async function () { - let vegaVis; - try { - vegaVis = new VegaVisualization(domNode, vis); - - const vegaParser = new VegaParser( - vegaliteGraph, - new SearchAPI({ - search: npStart.plugins.data.search, - uiSettings: npStart.core.uiSettings, - injectedMetadata: npStart.core.injectedMetadata, - }) - ); - await vegaParser.parseAsync(); - - await vegaVis.render(vegaParser, vis.params, { data: true }); - const mismatchedPixels1 = await compareImage(vegaliteImage512); - expect(mismatchedPixels1).to.be.lessThan(PIXEL_DIFF); - - domNode.style.width = '256px'; - domNode.style.height = '256px'; - - await vegaVis.render(vegaParser, vis.params, { resize: true }); - const mismatchedPixels2 = await compareImage(vegaliteImage256); - expect(mismatchedPixels2).to.be.lessThan(PIXEL_DIFF); - } finally { - vegaVis.destroy(); - } - }); - - it('should show vega graph (may fail in dev env)', async function () { - let vegaVis; - try { - vegaVis = new VegaVisualization(domNode, vis); - const vegaParser = new VegaParser( - vegaGraph, - new SearchAPI({ - search: npStart.plugins.data.search, - uiSettings: npStart.core.uiSettings, - injectedMetadata: npStart.core.injectedMetadata, - }) - ); - await vegaParser.parseAsync(); - - await vegaVis.render(vegaParser, vis.params, { data: true }); - const mismatchedPixels = await compareImage(vegaImage512); - - expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF); - } finally { - vegaVis.destroy(); - } - }); - - it('should show vegatooltip on mouseover over a vega graph (may fail in dev env)', async () => { - let vegaVis; - try { - vegaVis = new VegaVisualization(domNode, vis); - const vegaParser = new VegaParser( - vegaTooltipGraph, - new SearchAPI({ - search: npStart.plugins.data.search, - uiSettings: npStart.core.uiSettings, - injectedMetadata: npStart.core.injectedMetadata, - }) - ); - await vegaParser.parseAsync(); - await vegaVis.render(vegaParser, vis.params, { data: true }); - - const $el = $(domNode); - const offset = $el.offset(); - - const event = new MouseEvent('mousemove', { - view: window, - bubbles: true, - cancelable: true, - clientX: offset.left + 10, - clientY: offset.top + 10, - }); - - $el.find('canvas')[0].dispatchEvent(event); - - await Bluebird.delay(10); - - let tooltip = document.getElementById('vega-kibana-tooltip'); - expect(tooltip).to.be.ok(); - expect(tooltip.innerHTML).to.be( - '

This is a long title

' + - '' + - '' + - '' + - '
fieldA:value of fld1
fld2:42
' - ); - - vegaVis.destroy(); - - tooltip = document.getElementById('vega-kibana-tooltip'); - expect(tooltip).to.not.be.ok(); - } finally { - vegaVis.destroy(); - } - }); - - it('should show vega blank rectangle on top of a map (vegamap)', async () => { - let vegaVis; - try { - vegaVis = new VegaVisualization(domNode, vis); - const vegaParser = new VegaParser( - vegaMapGraph, - new SearchAPI({ - search: npStart.plugins.data.search, - uiSettings: npStart.core.uiSettings, - injectedMetadata: npStart.core.injectedMetadata, - }) - ); - await vegaParser.parseAsync(); - - domNode.style.width = '256px'; - domNode.style.height = '256px'; - - await vegaVis.render(vegaParser, vis.params, { data: true }); - const mismatchedPixels = await compareImage(vegaMapImage256); - expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF); - } finally { - vegaVis.destroy(); - } - }); - - it('should add a small subpixel value to the height of the canvas to avoid getting it set to 0', async () => { - let vegaVis; - try { - vegaVis = new VegaVisualization(domNode, vis); - const vegaParser = new VegaParser( - `{ - "$schema": "https://vega.github.io/schema/vega/v5.json", - "marks": [ - { - "type": "text", - "encode": { - "update": { - "text": { - "value": "Test" - }, - "align": {"value": "center"}, - "baseline": {"value": "middle"}, - "xc": {"signal": "width/2"}, - "yc": {"signal": "height/2"} - fontSize: {value: "14"} - } - } - } - ] - }`, - new SearchAPI({ - search: npStart.plugins.data.search, - uiSettings: npStart.core.uiSettings, - injectedMetadata: npStart.core.injectedMetadata, - }) - ); - await vegaParser.parseAsync(); - - domNode.style.width = '256px'; - domNode.style.height = '256px'; - - await vegaVis.render(vegaParser, vis.params, { data: true }); - const vegaView = vegaVis._vegaView._view; - expect(vegaView.height()).to.be(250.00000001); - } finally { - vegaVis.destroy(); - } - }); - }); - - async function compareImage(expectedImageSource) { - const elementList = domNode.querySelectorAll('canvas'); - expect(elementList.length).to.equal(1); - const firstCanvasOnMap = elementList[0]; - return imageComparator.compareImage(firstCanvasOnMap, expectedImageSource, THRESHOLD); - } - - function setupDOM(width, height) { - domNode = document.createElement('div'); - domNode.style.top = '0'; - domNode.style.left = '0'; - domNode.style.width = width; - domNode.style.height = height; - domNode.style.position = 'fixed'; - domNode.style.border = '1px solid blue'; - domNode.style['pointer-events'] = 'none'; - document.body.appendChild(domNode); - } - - function teardownDOM() { - domNode.innerHTML = ''; - document.body.removeChild(domNode); - } -}); diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_graph.hjson b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_graph.hjson deleted file mode 100644 index 2132b0f77e6bc..0000000000000 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_graph.hjson +++ /dev/null @@ -1,45 +0,0 @@ -{ - $schema: https://vega.github.io/schema/vega-lite/v4.json - data: { - format: {property: "aggregations.time_buckets.buckets"} - values: { - aggregations: { - time_buckets: { - buckets: [ - {key: 1512950400000, doc_count: 0} - {key: 1513036800000, doc_count: 0} - {key: 1513123200000, doc_count: 0} - {key: 1513209600000, doc_count: 4545} - {key: 1513296000000, doc_count: 4667} - {key: 1513382400000, doc_count: 4660} - {key: 1513468800000, doc_count: 133} - {key: 1513555200000, doc_count: 0} - {key: 1513641600000, doc_count: 0} - {key: 1513728000000, doc_count: 0} - ] - } - } - status: 200 - } - } - mark: line - encoding: { - x: { - field: key - type: temporal - axis: null - } - y: { - field: doc_count - type: quantitative - axis: null - } - } - config: { - range: { - category: {scheme: "elastic"} - } - mark: {color: "#54B399"} - } - autosize: {type: "fit", contains: "padding"} -} diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_image_256.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_image_256.png deleted file mode 100644 index 8f2d146287b08086081cc8aca548b294010a301b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9287 zcmZvCcRZDS`2T&Mb8xI2dnJdg5Hc$Jkd;kHwnC^RJFDBwPCR5}WE5p(@6Kanry`N9 zki8|s@1y7Y8-M)%@j5T}bzkdqjrVnZ?nr%Ib!rMW3J8LzuWG0mLJ$moLi<=!@K={A zwHSi%&{Y*BBOj}k4DyuI_Eo=*U-|NJwJ@%!mY}XeAwN}_TIrG_u;mW*zTyMMh3aH!gVz=^%zwPm5a@p+S{dQ_j<*ZU?cEK=bGaCF>J5!~4~@hlhvJA-69hx!iz z&Yht+2XW(ZV|n@US6Pp5OG@{czuVeX{33DuFd8q)g~zZZLsH90Jz_nY1Bg8iVlbr15x@dsa~Y zP-c5dtoK|B_veg;BxmB{@3;t{xFKpOmpW6fbt~|*{A?%zO^<^er>1-IDPbTC{r?rF z0ZZ6=<{<~eb`;#ei51P^^elFZuyt6tgne5x);@h;g)}XuEbUb|PVDYqG zt92%pkZ~{QBENe)={<^|SCAyh6k4dZjV|OMqL4Y%D*ISbkHHsSB&jVu?zh|yn)rR( z`*T3jEG7CC1><(opdc+!g6^llx60DSK)qP4!}}g<)cx9Is6Q{1d=Jx7C%I$OB0lwo zI`~+#_Er9V)OtYSjA|^Mc(fdFe(h^hl8&W?A=?tZJIAboIIlMS%YnH+RSTJd$-`r^k1X zu8;dw_#LloH=|TiZUdk9^7BhA zi#Lj8!gDV|!&QSzx)u`(2lu}$3I%kZ#iR7k)I^mhknBit`*RZFg2*cAOlDu2Ke=vH zsg^*^@6Jica%X+)!B?by$L>aX3>P*6iXje(o%ZBFq;35Bbxn`GG0C_aTCH7naGI)0 z@eD}>mAhk@Dx`d6;a$8zX9@H`i2g%&M{)Y$?V-TD>4H+I_Mf1w&HAd^-Ron+LJTlV z&~J9#Ia=ft{K-f*)7SAYyMeCi3$=*|=Z`i@9P4};MKlzu`uR3Tzr|K}^m?@F{|LGDT(g&cYT#eX!b z(o=;6R6`k3UX4b2Twf_MGT}q%&&u0ePSr)eS6!YW%XA*`!Ndvh*YBeAie;Cn1q>#rNO7L&w8I1J(s^XS>|0xj}%muY7zI`pn#H zt*S_!h6rhn8*s8vHj+m$XGe{`DKg=P>}`ca3!CmN80id{e61j}L1N}fpgmS|Jw3rE z$#%veAUvf6?yuU)q^45UXAtyew+91_?^^Ddgy&|Wf9A@I=m=MS@GV@Vj1O-H@wL~& zpc`*s#2oa11PLxSFP=6k?h{nw+Pa40P|xE?i|(frCESZ62i`s%W1xRMNQ{Q4Y+fT$ za&zf*jd+wGI-Eo&-KC}%P<`Q_D*YduS#>Xxl_|BEh-7{UH>^~PW(n(U$Jakcw%qGy zIS}qrHl7npU!>@Ioez29O5Wl5zWCtvaQ$L&jPgrH6`{qE4O7noT1f^t0$SAPy~LR! zojDcPzff^(*gj-@-`;Domh+i!md(^qqf3Ut4vEGUYm5ryZ~I!VpviBpXMEbrMCk-2nxId&_>0~eJIm(Wo7g~2 zv6|PkQHtdxveegM2}z{4JkHUx$v|@VmWG=D(om@vlim8|FvK^;XNAyawCne&La!y0 z?UXDvLWPt}ojQtrlD@?`vbP%iLhZ>l_?tFM9d<^DsHM?p5-{1C>}MD3nCKi8Sgwzf zFSQ6(aeTTMo@hzonif>u=u*t`sa?aJ7I`{`R^xIfi*P_5#${-TvUXJ&y{Y+>FA^Ls z1mf`g%)bYn90XSfdwZ5RGxU@|?9b3RpY8^!l7nYAdihdD++iz+rJ?IfN5SgTBvFu& zljF%7<+5j1xOX_of|A+_gWj+-;Y(?@jpeZ z7NEDh^kWAI|-Q{-L2XRh< zdReC%TYmOw$Sj^g#odmj>8SznKE!tpr5_fV61N%GxqAldW@%OPp;Wmi&FQJfARWr# z&V@=(ZF*5cAC<$FvI_KCj5ar=iLv)pkWUGRp0#;cveW?gg54awBUj4&x5)SPyJ@T~ zUQFD8c8}UNL{$cI=A-4cCLh4##{Gx7R%k_@ zR@XrZNYByh*7sQpB;t|0?=QdDZYjm$H`inM%HBOmifb~GIv;jMREB}%MIA+FA7aYK+uY~%DxD?0yC8QX+ z9Kk^<(how++3hKHy#UakEK3-;ONCyOE#T%ut;Hv_HE+5$@3rS2beQ^`R;d};_y^Q% zM@?>s_EhU~^MmUmJ4bzYY?qQriL!hR(^hlx&bKEbz7n>xIrJi#v%|eW5eseA@Um7+ z)y!9i7vJs}U6G@-5qJ)|1?$ol`JFZFQ>_IAn2bVE(z$rq(--YzMSfkU=@2_zb&9C+ zd7tlx+1wu~ta@Z@9N^_~*OoziKG^XB;n%3|;xN|yi@#HD_A2SL3OMqi0$}I!$J2Jr zWFNntq|Sm-Xt8s1<+Q*W?X>%p+h-RY@1Z}z>~=vX>dPz?aMHQ*MTorpUMR$U*LW*G zHaLx}#kjW?pH;^6<^Rxq^BC-Bxy;l%u%hzqz>3h?+?(TUtM(M>aR*6|8h}UPrUj_f zAI=}^#XPN^)&kyM_qsWAHgjHJ(;AT19og?Mwgk z$Tki8AHSvC=XRD7?5<{b_UGVoTocNss@PopNtEg(BDX5U&9_Swmc3q~Gl)0LAHAs8HXL2qyaC{Z<98BaAruamGU=L| zEsPUyGfmR!y!BSj2h zU6nl%!UCYJ$~Cd*k@S?`!VdoOS!IE^|ETOl@E^YvO$Yl&gP^eW0bbSlSHLciBIo-L z>xSZHvMJ$oHP`FGzbRy&HuG?A8(xG!3x~>rLr?iY8m&QfOiLU1x*fLkWiv0l)9QUt z5@qR`;0PP0SIp1;`1Q*wZoTgjeZuN}JaYSfOm<&^kkf?c;Um}k3!J@;d|4Wg90n~3 z93rL|eDI4jp+Wd`>-~20dSGpO7VY};Vsk%L#7dJCG$`PNEZ5w4d6KqqD6`6Q=~kc{ z8}ED9!2xKO?_TeoMbC(v2oXzT1^B=`8Oy?`+mU60%*-^zp@`Vvdiv1BTiVxF<8NbB z$_|2)7+~!Cp6edvDZejX2%rp2U#ay>F3lq)7pE9e#Vcf`#`tq5-H-1n8R-E{&qgvl z*9BVVz3DIUtgM+`DcZpNvw%ZC*%mjduVRCjqWbaXV6x;h(Id%AQio|lvYYju ziv>;bGB;|mgw7DT+=FBq7s00A_1C?|QXa@2{>&3?W_61;uq;`bNcWCxYTB^pHcO0t z1v+HQ05ItxZ`Cz+Um#2Ueay;GRebgSlwo0`Cs401dxKjKpFZZhd zsNGe2b}bjcXPmfstHCj~4NO$5-q~hW7O`fY1xciMlrCh2Jmq`^jF12ofQl%ci5;%r zS&8rYswzETFn_`IJPY$T@};nALE&b~zD19GmzwiyLyO4`=H(s{hzc$=@4fkonIHGZN1ULSP>nCb zcKtVJ(*Gk#ym1x37O%g7qIx1!j-^F#pV)MF)1gOXy)`3|+Y4ODB@KokeWij#aH7P{ zI1>zB)fQi_0i$X8cc&!PBS`^tAFx?mlHJ!xAEXkjX)9+wB(IJGnTmua@U3 zQgp=JnX!~BwLY+#GZ8p3;nL}B8R zFic4jCu?@LFDVO}f3>fEPKgH?FTDc1`BO)Iyp_0-_i3aqdGI*YsJf(wheIj@nWd^1 zFMTEQdMUd2Q#3t{brM6Lfxdbw4vfh7>1fz5)yLeh@WNIcU?XJyOGvSM;DEBTJ~ufo z$K}6eZ!*lc`gOKv^tRu-C6Q8F?DL_j<+Dv`OY&Sk#h{>*mz|7G*r4CNE>eX?Em_d^ zNIma7gt2kH{Q5}{^Jcx&1wvYAxC$8FG4T%GyDTCbhrOm*dZvDl^~-HFS>k*tRI*&t zO+h6P^E?sEuDeTx+=qWEnd@~`KqxUO4r3bvf7_}4h51ei6snB!tPx-p)fK$YM%zr2 zGYXXc0Aq=R0!%B@Q|On{rq9Ia>ya`4A^s ziMe_DA!BOyX(_iCAWW!k64B6mVOd*C&c8%5BNs`%>3()U_kmw4cmu8C(^YqbC3Zhc zD!k0)Z4wMkpuU#V6fduRc30tG{_c3%CoyKLv&@1h{b{_RrqsJ~S>Hx+7YVOtlg^Z+ zmGY%t1MjB*cEfyUGO5H8!g%^x%9~QhZnvTJ6*}`07kEEf-y2WU4`l2TehjdH^k8-O zg{Yo5(uOxFWeS|kGz96(iE+LO$noL>zg>aK%(Zdq&NrGMT?H{27OB!;=fp^KHm`{* zDYLlpl3n!+iCVDMWo|{w4~9i^d|^>Y0VbSu14mi8Ur$dG4zE_!Z*D$M|Y*LF+tuSw~c>xAw|Z$L%_Ix&hxa@;O} z-l{BU{%8oJw(AnSVCNwI(+fO?9=N|Y=q-WV-^g8m>z1q}7y)5kROuA`&xkpCZ(OE0 z*pNEQ`EvJnpQj)K8W@46r{t$#{M6=iA4m{Ca!hNJ`Rf*8NKA2(dmd$kksy?xtXxLJ z+1i)&i2f+_{H*7l2PFAy3ST_LNkiO#@vA1_AQ12J>`dMkt2`(EIWvdxYbUnolo&lu zjLt&ajrw-q^}!pXPAqb4HCWESv{UCdIdO6H;6Zj{H;;{s)-M~!e)^yD((=4kh`2PV z4#;NByYBvezsfhvx^`j|XJCQE=>r8r#z8*Zk;~H1Uc9Desx2tT|J12Ec!|p97`c6o zBFolBLh@MS_BjwesX86Mv59?qX=c1!3Dz2%~jl(3w`94Pu+bQ)k&VxaWbpHHf)9wnWJGBfrYJ=m!Z^NOaE1g7E&|N}PdW zJsNn8g>ysmsFa8T4o^^9Y^Y7?adC`MB`5>kQwJqv+1p(fOLdO~{-Y5th}8Bu75ez3 z73NF`fEkTEU5NS7KjSZ%ZA-p{PvKS^|)9l05`0)woyRoe83RhVU zFDRbz_x~FOwIayyc_`m?Gr#Kz^bML4SJ;;A4We3de_&K{c$rIj{?F>#U81m$4gCm5AspFScyYnaB z1x+7ytPXSe6?|dGTPg?8iOI^`lw1Ash;H3Xmsu3iyK5f?1QU~&n*+s@G@qchpBKOS zaT7 z-S0ySWo0*(!88!s`-kV!AXG@l7LR~=uSR%2iW!fuMW%sjNYfIWeFCF1NpOheNBseV zXJW3bK_cA?=N_sN^bv*cs*r#1;yd*444nNCl#P?V%q%H+qw~3%N)6y6P-n9N`1r8K znUPK1>b3hpU+9k*kyC3B1w^FTPMedcy~-#{fCqTT_`!8VR`$h7xa?Pw`p}mok4$c_p*?K6y6#$Bzp|r!kp%@LZZZ6 zF5SQo`OPZ|%u);(Xn+i^(9}4y55#R#ZQ0E@CmUl z`AB9wCgsXMIC#Bl$aT;YnuAOomxkIE26&24sPNoo)RbXu?XXJ~lZn2jp(-ReidIt^ zr{4Sk<7f!>JcqZ_<76^|I3@Pd8Z$HJ6|Zq0(#6X{S$S>fDSD#_+ZD^yerGm$0K{-F zT&`&XHAjQoA&d818RSa_Sx$5&aM12d!V{;{<{d=Qt$L~aV8aAKPPls#J+RiWa7E|- zb|VEbKV#qP1BdK!JYeLhr_c+qyUq8nZbHPpD%``uOPX z(oiM{Q63(ke<1bL0B`_#fj*niw&(t&jgDmF9DPo5% zjs~I8pbew?vzPQgBUyn?YceCee!+JG+Cvx&G|ID^)k4=oT;FmqixP%YKm_5WGL|i= z6v$&JS3*6jo-mM;MrM1}YR|)J%puoi}Ya1oMFKQosHn{6^=Q*0x_EADueD ziYO{53jB8ujCU_R_zWi!gJz9+Q4$Sb@(jrzaZf#y1}thYjQIkVWFGitRHK+f=1#;!^B_THOf89mw(TajQfq?{-04Yd*(Xb2Gv#l42% ziH9=8NdPZWp_)WKl-QU$1a=Q0x~P-kGO#WI{1EP;#RargL^W;+L)7X&%!?kkat0Zl zjzb=P-8lf;0VlCDZ|R3@Tibgk4?v47<1lgPn91<57rr8i_t)8#`NLL4vq-9F z;*DS9m96O@w%Um1)h-|dgv&NZkx~BCzw@1@9K^L+E3QQi>gxMS@{9q{Z1~>)%J1SJ zdTD%cP1bRrE&ju!usQRDiK2!NSull4^cIUqC>OlojY390+z42^y&<|ZRbWuowF z-rkfxw!U7l7R6&5R@$SWyVs#=xs;nPIG){xL#r0L3vV%exkTJ6@qxQvT=Iy+5_i|2 zRqhQawBdVqk;COwQIv-SOZ0N6r&6jt&pmcfdPnJz;C=e1K9KXYzfMX=}@~`e9~wI9*;oAL*B-mf+Ly>M`T29bC8lOiL@}w;wge0WV>9de}_eP zZVMU*%Z2r_D2U3FY^vZ06dxuUg3oAixu(O{1idn*19|L|;Rn*p+2UzycF_=(@`J4# z+wAZR-=&L3yF;SyaHMw&4OoLD)|&mnRfJbyCNcd4vQo{>-K9m8#P~9LJIJ@yN$+(Hvpm_Q69#QNuDP8eP&ZvkAjyx2=Uy)aCBR6I6$n~`NaY|8dn>(2vh10AOjdAi z!4ao^f@Kl}VNfUE0BB1HV13;?A>&knH4Hcf)hmz9*Pq!H1;oVc&DVCVI zEpiWgfY6iH%CLed?aMVOzen-)ZE@iGgRTvv4Fv**lo37SE*+){YwkIAklWHOw8%XY zqJbWIm`ucBxY(q`t<8|@E@&RQ7}rZ^q0Ra)OkRg-ulZ5yf$&d7GCb2jT;r`Yka`<^ zHYJ_Q9T$92Q`RHC`yb6LzB}GX#1c&s&4`qhlZ@U%4EPk0NdV9h>XV_Yx`O+|#nU^WZ#oO^=rme3LiPhp@GwP&L1!k4j)pMOP#6{Y zGO;fDfOAaH3ehZ!hVBNC;m;tNyN1T@iDA7vD%-P1FJt&$Oe=|OIJ-FemQ~C?pMPDS zVlbwSnQHQeq6srF3-!T>>mUlJL%Q`Z^#0xLz8K~gpF&*6zw|cBOv)U5&7DqDyBW(3 znNuRUa+K@mifjX$RX@Nik<_e}uSVWFUjK#eBfC>_F_oN5;=9e-)78e5fnmzHvW)``A;`>VEzmCfH|BT0m`RYr?1GMk<8aZTY zX17Axq+%}VJ58FIc(Kw?X=x_j+OoHGii;F`J^wimy`STs53NgKE8J-;Hm7&CJ6|3n zh}#>}xwNuAO>;l3yGVz3rqD(kp1xx2C_i&vWPHs>J5AClz2Sv-veZw`Q$@8_BsMd( z85eG^weBlYbiR2&-dMI_ZF;=BWO_XJrt)az>$>m#fWZE;yRa#-=*!aX-u*THo=rNG zIqlxA*fBoN#B{=nKS$};f!7+OwnWlXABN!8)d?6PCx0Xz%JV9?dKBppWah>E0UwO4 scU;X0C0K$cU(oZmTa_GsggIiq!d=8NRl=ox@}1CCRb7=5Wt-st1lx$`BnkK^uIa0(a9G%K9{cDU`TD|FS&RjA$ z!&Q9dVEHkJHk~3*a25px^5-0-Eb@|p=2^1@d@?D5P?#f#L^ksz=b~APorhgz=$N_A zifwdvueC}i288DSIapfm6?L`j>_Pfyj8_V(cmofz|>) zR!f1H@fex}A>G4g-$5(K;a>qiPS+aq6^1m=@1$pRJpSkB3?~YI@6P*rIHUXQn)-$-Sj<2Mz_DG&`Bze=fQK)C1WgDj z6(RST(wxEOEsfqgrTR#SME~E+6&uMqF1zpuJ9l`{H~W%B-8bj3jTbT)5>}UF3FA ze^^^^A(>}LaZ`-bsWS=s%G_iY*}TvC>(h?EZ4dJJm)yc#t6YN-)7zIiUa*!qJ(ZZ8 zxH^BN^|lNX`L)KvP$PEzFMRTt*}U2s7|6&g2rXA#__n*O{)d&HDVY`0MePRXmiVx=*?U#07Ww-I7E#7nABoQ0shRX6Pw|Kf zddQ6YS}+wvd$G2Xpgz03^hV1t`0cIR)$4`l$nqI9flEYE0*9!^5q!{B5@dQj+=e7v z^ddM^Z?o$Uqy2>tjQmv1CpA`q=KAU!kiaXuUSvL`)O`c zlHMn}*y|)O4n7tC$_+_$?!q&U*JG0FYDqy*vP`Zcr+60QI?x zN_sD=>u(wbelOeD+9I?SJd9jAWj%_Dukd6e&4fI%tfGYH_b7>aegpJe6STVd6vHcC zQFg@nizL4E_3M6}(H-sI&C50}PoeVC-*tVE>ygsP?+>rKG}}ETrF_rVh~=bcV$AWd z6*70}k908dbgbHUOtO^R(Y~R>(2pSbL7H%xM zw)nW#e(*h){|B0sRGX*u+-cRtTKx9@ET|!_@in^(l4(4c=i0ns zhTycP91b$<0vo1PFD%cR2zwXEJO2z_Q!QHZlpuZQ`;&8<;_h`8!J4!Y_sR92m|%@Q z^(0iXTSrUGR&6+sjA+bbS?H%m)>^a{pL69Ckf4sHfOC|My z--vXMWIN3&?ugA%1w^KHA|hf{QTNyEaoznp`!W_c(Vx^oK(3l@c-unXv={Lbmr3b- zw@3K-X}x_?aQCN4&pagy(0dS$4ZTi&%KQGUXjU2$a4Vx%YA~o<9q!%x!`zhf=2@=KQ_h%UqnFOQ9|NxP z>Bi-++X|@D;u(%3-P4cqnC~G1w_3-$heu0~_kA}NWN&3N@RA!D7LiA#3V|;jT)$Ca zPJx+zwbhHuH)u$*lP)pG3ja{&;n!LbNK{%+t?$cu|K;38@zb}ox#)pJ45FW%&;95g zeoE)8xOp0DFeo<6wz@Q8qvu1>a}<(qDp5E1T*;o1iF*tDU&ZtMl|Cv&qiZHoim^^L z(II5E;gr`U71Zz2uaq_OcUozA*usl=!zf_$Ww})m*VNb2r8+4>IHp?~Jd zr!OBTqDiok7`pP*_1qOrf;F5&etAXbGb1%VRv!z%SW~|pU#jKGqq>nV&R&_eV209`};!k(zOdF?Z5v7 zTg8}pL|Pe_zfSr)Jc%HM+$BAKtV#^%9ry zbX>M4e&@!VA_b0sU>HM+;0om=k0BZhu8_PXjc{~-e{){;qECaVg=t!GEslw)%krebwZJsHdKYI4f0HIJ4P=P@<|raTtLOsn z6rcH^SB-J<+Co4kR-Xo)FSvAex{|C2Oe4|v*zP9R9Y_q>v#;M}NkYD097EC#d=u#p zpj;5!^1C4*9w1L{Q5t3N-Kj~aaWI&c+Yk45RsO|Q7();Ubz6X34aUkQA>+*(-% zHL0oP6f1o>4!yID^i{xgLawhOT8$%p?o7{i=Fva@Vu%SW1;1bziXHyJQCqB#oVWFK zbZNjc%GvJZ8_&!iiR2BU$c5{rtrB16LWZ@t*noSaijrr{2H+==iW zj>;6AIPh+jTQpa`(Gseb+ZZgK;i&X0CpRZ3%o(%^cYN!0@dNrp&z@Uf`J9T&1$Oqy z!|Nx_^_AgD#(s0z+&tU8E4$tm5-)oW^4F&y=Kh+V2u^v|*Alb%Kc`;=mLJ1L*>jtp7m$1`6yU-f@cz91RV?iRRR#e(&L!_+yk(_|ud+cw$_ z71!H^sLzvd#gspusPrXWc^?vDQ9b`Frpmc@KNUg^o~c76dq>LgsSxhTcexY`&bvSb z9%V)npE?B@U*dIL3j!=0wHI_+V|sIUR1xU6+Q63BMcwt>kf>TYMeaUD(b1L|>|=wN zhz|^gzl3S^4?KhnDkO6&a+L{hDL`s&Nk7_aRbOx{v*SVXyTO(i5{BO6Lx`}gmWx!A znP{&{%ea!KO5k7vaImMmyRjOGBJ#{ZxAZr=jq{z2)$SObRv{>b${GuaL#AT`)3bEy zB^ZgT(-Bzb#SELbP4fNUlS7tA5M#kS zy)Hcq>3d52EoZsW$K;laL#ubDQ&ITuivtZD%7U#r7&U)jXL`@1-LFm`tcf96r3Fh? zjw*0I>{fW$$1Q71`8E;76*hCOa(U;4Zl=bOEch*&K>j^JcO5n4-iX_mSal}h9rZWI zk`QzZyIc|XyteqDc~me{!BB$2_)o>4`eKY)I(Ugnh{V8E#{HkNys_&k+m%sb0%t0R zoLy(c`Ohgm4CSJ5Sz3zUk8`#w^!$&8C_v(qqv4l8EENn|4$)mM{=`K&>AiImahl3` zG{-DfR87<{68RMF;+l$1S7v(+%U>p)5fDkUjPnj3cBrTn*q)le+@~RJ^fY>)Q6bSU zj~WyJW>B3dCqxR2`L`k75#jN~DoTSg<)k|GOI(Wfmuy>FjKb#q>Hhx_U3zm(B5&(Y z@G0_Z7nut#yOjT2$``4u%C?-mfs<<&X6nqVU*hX92jOaQbJB*J0>idcpuakW%fq_N zoE~OWx)jNB@}Jo5NHLk8W41re)nH+SKB)ur$wbsw6g!&Fe#yExa)ib>&=5w{h)S;0wISywYIq?#&h=my$4@p~&qeX&%5zkOakyH&Qgzo11HS~_ zyv@=%T4}!QB%!r+zvxIk7FpXvLqbqkU7hqY)Q6 zbB`XwyB$%4USw)|uV1;{N^#-#N%SXJ8-k6IBOM{v-y=In^d}3A%iL@{PIY#<-w3Jn zC)<`+vGf=if4t$`rokst2m~hUjmj)Hs#|TTMNYn2z>7pV+9XmFyJ0N9oO8 zK=Q7uq&13%<;4qLI(2YEMaN7$lxMuRL=lo0yDx!rvmdWtDNw19ag?XRrL=<_E%S3O zeptt13`9Qacp=TZCAb^Y8=7+~mh-F-hZh+x?%oNXco1a}B1&}6atFgr{(QxvwZ=|> z3Vk~eUnM6;@I4S_+0L|zWBl;u>*Ys6T3MB~*XpBsJ}}#+weHuqecZ*U`2o$Gydru^ zUTU-XJY53Sn0C8lTza}g86_yP)S+WJ{2D?Ax%B=>%=Rq!%<~o8niQ9~mJLo*zD>nn zFS#n5UMzSN0dlApc?T!OWOR<1oC5LEi#%{slA{;yL2uyd+Y6%Oxa$LzL@fekCDEQZVRKUMkV=P=6{_& z);%1+-T#V{iXH7b83cj~G-lH-Ie$U;7>PF56Yz02rQV8PXxlVh^Ht6}5h zJF>p#=g8jCbBTT-Fmm);^fOgLKZwqOty!HMZ##5Hl!e~Y*BwoCkW?$bAXDkewlt-6 z>}1$I{Yoi;1oT2Ia`EUGvg5pZqBtI;<@2Or>WKFvCBbQAg_zKgEc}Fl2HA$Q{xP$b z7%E}~NyiS`6RUp3BN_Q6zCtH9JeAzgv4p$Non4m2`S+AIjZtm+1Jgr4^(^`@)=A*X ze;KW%BQQ>EcUjRV>R#v%nM&bbyE&_%)*dtuUEi$t-yc3uY}EcjamE*ZGe27i7+N1?P>a;vCl(I-_)Z&kw!E4T1FgHsKXH&0}1> ztM8k-YXtGWTQ1v`ud`HDdxCfc_p%-PjjprY|CZr0kble!P2UfJea5;!tE|w^ zno}SWvHC?YN#woQY`c`_>31EIj8gz-ZyXJY@;?Jo8au`0JzgeHX)*Dt=3I;fB6Dd3 zRUg7O`>V9Avs$~QQ4SLr4!%|Ds=#X_j#@hdu4r<+!tT|d@`bq$jo9l)ntr370I+DUBnyG?K?e^o{zP|B~tmuwwLsv%(Y>zZ}GIc4Tt?# zMDtt!3eQw@n~sHXtE8^Z)4Gf(^rR>FUG;pwrqC+;Rh43ZlN|s@=YpaH{n~PRud;*1!CkfdBoyM13^SLghj#&K~Y$EcJdKnd@#T;qIh7Vvjxd z{)IWEi}^}s-s{3dk`=pl$x5Ba06(b(LjN9GZ4Lvm(F@BpWGOgc{dsnLf;-|)Odz^y zT@dnaE>WMqD9!^K7$(@pMKd;@e1AAn71A@}weJXBTP;?l;)yjs;xfYEwBKx}b@0wv z(6mx!E-MPovm0=p{qx;TALLnc1&85N>bVLCYmc&@6xo6;Du#77@3wG7X370!vQ|r> zPI?#8aOvQNR6OLU4YDd_-;7B@nx17wQ9N)dYz*xMsaZ*w9t$pm;kJfD;@%n9%2@oIF_5MBNw1AC(*0mx2S-4Z^(#nTxk2@_=pT z*v65ZI2Q4Zm(B+K!i(dyAEkX>qq9y>eAZw5Qo^f5P#QM7h|(nS2)2rHN&CMCWA>X9 z&}j(rk?hd<)AQ8t~)TsDnI-#Y3aQ=EAw{ z)nIUTB&hL5@yTiL-Bt%nK69+1^?7lG8qm_sRhK0)~sa_1DcaJcqy%-YSR}< zwDl&BkOv^~cF>BOGlbxLG)`d&9?;T?eTgn;pia>s-ACos@kbfYzPDsZju#pMnuh>5jvEf}=PNIpREm6q`Hnh4{60VU!4b<%lwC8iOD$mb5wJnZEneU&ESgl-E{E0K(#sHeFs;S$ zdr%2=ykNjGjYZW;53OX7-z{DnAp7T&+$NTHJ zm@tYAh*OswibtGROYWE*ye9F^fUDhLaokuhwQkQ>N$)3_b1uqmF1!)EqgHu*uGB?l&a_7 z@YfgUZLArWIjjB8$=;**WcM^E zN*&F;9;(4OQ3<~bm7dFN8{;z34Ats>4TsW_7(3E>cbBxD77c34!UR5s&HNdGdqe<) z$1bSXsQC6Qe@Wx}MQ7Z=R zl6G3b$QA%N+%<#P`O~UQS3NERwO7M&)>EM7?8$%nm+JNs^&x+~$U8TALGFLREp?3^ zWA|$^pQWs>3Bw`|bouB)`-hgJ+gp2;+MD)bi8>g=M9{oy5}H)`J&c7+WFmAx&RK?Y z&PY%J^`*M(%1lM4Ad0qJ%hoT%`$S`=oflISolfA@<%fMa(h48 zv$OOXqS3CeFU@`L2cKT|_(I?5t4EF_oB+O8%?nz*|58f4$JB+uCB%yakUBz$euTO& zAGJTKW%osa%k5K3Z0pT_C zcf>fI!>&h`*7K1u`~~0_Hrk#Rpf6ecd2dkgws5Dnle}VuXz^87MqEuoNk+6{D~ujq z$=;lY;L%w%966{?)|`|heE7bWG59I~;r^v0I0;>FR-i#YdCXv%*ZI}uLRTT#9p-zB zkKXgql>uWYY%p4e9j@{WzV}a{!q|W%`+e=PkEF*&mYY7OlfIXs}KexK>w~M_2g+fxB^bf7gW%dP5bo&wI{cE(JETNa!3PSPOI&B zOnLGQ?sBejdX`CZw=ykMyCOBJi)mG6kkJ5~&{@tOe**?<@O+Ku)Y$eEnkzgxdUERP zhMNDaXf<$M?@X-mqsmT?q9pcI&Yo1(=JEqskw@y4_{IBkOQsaR=p)SDydN*9`bFKEBwF@boXcM56({Rlm8 zcc!;~!!9F!)U#|LNEkDl2TXw(DRrzvS-#TQ$gyBzp7$S&?30J<{u%*qictGl672G6 zTvX#RD*lNlHD*R}MILLXahS_*!-894sK>V>mZ4n|injIQX{jKsE;FHSGB;OQ&Ws}gln+&%rW!GRBlG(miaxZ(4a4FLd-r)HnKdQGQiCJs z3K_y&H)wz7CB0MLwxpZLT zl?d5*0w51B=wkNxtZ|!Z7$_A-t`qj-*IXEyiqaUR;(Y=Upgu;r9PFJbla>)(&Xa*>vR~%JiGnv1|L-YvoVH~H;&bjX*a}cJxFDh_W)&07D$f4T@fx8f zc>fr!_g`8Vx30)jAa;eu#QD$QHUsFq`dVT+H}<&9)d(0EN~;e`8@X9?e$kMA-n7P7 zb}l@~r^fHP3r0tuSnsDMMN{&LxPc2GYfp?AWdz8AX7cuFBFsZ`gSX>4;ofFG){x;l zAO3pj)%@C-6=2AUz>wK8j;$VFuklQPj-Cu_v$ZNZzWVMNGhCup_Ljxgmq5qCcVB&+ zc9!iwl?RyX(2@@E6<%tU3 z*q#d*0C}?G6IAc?+2(K0yTpGH;|)`RuQ%TIna6G@t&{uLlGe{~B>>RfzLuz`P4#R5 zjc@DhA;P-C+It|t9XF#xDppKC5lml3(rr_#m$dM`|Q}_eg#;Mv_w`+ zL?qgz@rqdXzI`YwldGUOc^+%>T{ws}<|$GegHH4JDWHQ_bcNdQl7kWDzHPh;Qy?n>H2|g!*{Rn#Pm8gSk4_=qS%%-265hI{gxyA~HCN%o8g1nR7Ya)?0K(Ap>_%0SU7CpC#v+A~$-`(N^AQBue8%&eODisXI=II97TbK%h z-DRI?63RetXD!P|yYRej`@)?9+$I_QcjMOBf(_#5c+>NW^N$+zEGtf$n3rvb*O<2S z>2Mu^Gl0kx#VA z$MWN|dF~ranAjhP)$(>+Lo{N2kAz)U`nEWAr6w<6T?Bm+2NZ>T!qUieY;+MRqOD4s zbw}Nxv54FU!35$@Qm7 z1Q)7fE5QN=hK1JkaZuuxeaW@T_8E6fAS}*1O_7Y9bs`q5!*u&vUPw~EN3A3f$OX>5 zG#0P9ZarT(F7fky8S@wc6IcX(g@qQI|LxC&Y)OVyT*$j$_20iS^KLJBqwP<0VD%R} z6TWGFeT<=i;Rv_h+>QLYTHkZQnes!q>MUsLSNJa8c6_s&evDQNqZN%FN1{4G3c#R& z6Wuox-3Lb7u0@P?-)~5`H9|9S(GXPeR!U9`hj=_O%qoF^LMJ0lCG6>2h7<#71OOH+ zydZwB-H2>*=JlDI`dUPyA26fC6#}kiT>Ms;zH$IcSU;*S5AWeo53ZVD*GxnB$cp z_3jD;U3*EeKgGp6;^@c1AV)dhXL82gcvTGk6!dy{d0NBWe_ zfUZ-H*r!NUSS^0-cz8CUYE6|6}-9WgI-fGqM@BXP-3BkevyFBGG#+ZxG# zc=lw$@}N#6Y3-vi8myfQI_*tuNu^d<%u+mcC)cP-vX*w z%7R9#X()0lSW1e3A_z!{#hJEpVW_uAry;{Jh%e-=_-B!qzw|CqG(EG}g! zKg}-_A-6nWDHN;4B?w}8%#m*zfz#FY*|Nl0A1}E6z%^%v$W|M@8q%Io=zh5sv+ET@ zPx~_;;ABHucMf&`N{?_NC$ZdyJI2QsT9tM@?oQs4aD_0dO1JYg zN@#5{^G1D%#$WvI3p#46rIB*-n1AB+67|NDZ)9DL-XSQZz0~bFPLaP}Y+1`|bw?8Y z80L!z+Vm21UEBS~3{fFj1JgV&fye~PhpurMW9Rh`t_m3pO8>uvwp1=XT4$cvO2|{-+Qo~7BM6uY%$U8qDVZR3hSW7Ch^sqlP7 z(Pl8V5bI#fsosi3ryB!v%4y^@UkCPjQV&Zv_94e1gc@|%+G8Au%uG3&$nIp1yeI+h z1YwLC99G|sBDgn~x0ga8FZQ5Y)$uzv07M2D8pC#9K}vGDyUWj(^c376aD(&%8j9>c zL|;9HD;u<#ehgp`-W4nu390IhX(l?HR|h#0w&%fReAsUL-jXA#|MDRWwKG22)W zsTutmlreVZc5zW(lnLU%#U&1fo(d=M*-EK)XPUhEXKh$?euvZ z&%fQ8&a(gesQmoahP*Qc)sq9$x&Sd@MOXPEvMT$O3(EtYuaz{&JpH#NRjZzv(fCs( z9=Ao}R#(z0dHA#@*K(!8(J=}iE=EThGz7S(%+&92BiJEiHr1GcLpb}2pCFDfMKL+^ zuJJPdLCWq4tbxo6EsRF&hprrqazcJ*-WaV*GiY%ti!o7IU8!kds{vlE%PvEv!m=p zEX33t_BFPr`b^E`)97@4a4*JZIB$+>4106Q|GaiNCA?7Dq)Kf0Gb&5=Gl%s ztNz!cF9MM^fI$-0`z4@ztB;+zpjb^TZ@Jif)4s%(_>vaTm_)FrRFDf6Rex(B11z;` zAiUcoc&T|aTUMG_lhp={Z-GvmQO77UrS3jzt;Vwm3Re|*9V^kLrb>7V^Zn*0mFI@C z4So@3P5s;Q23~(7-i@FEsCPzn#v1X7fY}%Re;$sR&``k(Qst0ei$p#O+nK2fQQh(|ZL0)tlkS7x(H+#?fJk%Cx>>!%n|o&4a2^1^Ae9W#HJ zqZ-bUUQ4mnTq9nm;qL;7*s|xjBKNUaN3Wg_|RnLqRL54)|<4mrLLR3m4>pjr;)Ap{tqaj5>Q zH=gGYgirt4RjLU?Qi2Q|mVDzvUPRUMUBNm|QnxJIIHAZU`i+xet$wkN-Iq&5Rx<(Go-4FNID{dqYr?SUI6t`mu#VQYd#zP) zEH^a@K?K9P*F-fvPymZZ@OzvhH30Khr~e~- zY=F0#VL|S%(Ib{Xs2GA4atU{@&kGLA{(XT~wEL11M~%|FL-wCnD8mw&k^SmE=5>FJ zS|IS*$JA$o9He}9P`KVOlwr&t67~kl*RtX8oVzfC=0&TWCo`u;Ia#G<8tmX2@{bc~ z6I}-yN4y?(BgeG-F?KFR0&ubO9QIwQO*#*keVa;TuUyv1`$%urKzu5yUwE5uMkLK)S!+otzW4obN@)uBtI18UM4Mmc(}iG#3PB0 z7QM|iz_U?CD zkq0_G!o#e8kN-W<#Xo9_an3bbmRop!o}-Rgmunh)@6GddS)Z}nOJe9}rWCnLL(@}P z)3@`TslePq6?IOA0&z>?AO% z0L8-n%A6%q3v9dOxnGO6HL=K508r2^@2H+V_?I^~CfY8^eh~&@A$*q;Hej5K-tMM^ zZ1$!pz@fP$fJ8cCb1Q5d3a+~viY4x{`0MUv1M*vFbC<{wfgVp zg|??C(eG*M7!zCDjcGL#BX;@02;eE*+eD;WwpHB6T@U%|ngl7hidx^`i5<3iH~M(| z48K%Kq{JB_+Y|Dao~o7zn@4<~9k9fLismY84C0Gn9<6&z4ah4@Jx_Z~pVk^2`I2yF zS6C!P)O$TclJoHxJ=RY0kaWEY6Z=L7R3+c*7JR4pChv!=cEgxI0tyFawi3+vu9Mx?M@eQ6 zOn<9MsuSR)(**ah8gG)WD_iph8u?`}CfBLR8iLF#NA?a^a`k^>7`SCwwymD3^$)Zd zwkx>Z)vIIJjv&q~i2-`U1aMajNC3?*z{BNdu@C1bDn2?faIz@7VfV!J%sbi3_)4g{ zp+vn>RUW&p%{A)OPS@b7I@xVo=EkPV8^#1@l^AC=@9Fu7sz49q`y5+lT52%D)Jh%& zaIq;vlQ`5RKXheoOR*{pDGC==B?MLZyCYamp|B(*af}<}VLqJ(YSP=>bB74M)?=g% zOv{eE6wsXs2=dU26O1zNEu)(M%1qqpAlt=5wE7Mus7A7s{m$A4}hV|X4kF&eh$XSl($EZDmfN0xnhtL0qJX|$OzN&=&1gNRFXHOUdvfM|HkQVos1b-Y4i(=*L zU;rqfyq5hL)_J=g?eM!9XjCVRQy$T!$!;{&?8^)=R-FIhtLa{5q(CefLIuI-tQ7jC zPaF}ApWtjF;iV&i)y|@0%EM%7R+Z;+dx^0=4-j83o8A{YgF@GMMk3E3?*~~iwyMAW$+ym7fF4sMhTzR~FWYVu6`&C+Ojnw*R7?>pOxId{7Wxs$l&a`VzW7I}Vl z>T4wD4(>RPzykMz^Hy_*51->Q)b=~Yt|v?D`&T{pfL!#P zTJhu3fT5nxH!+ZLLgRf*1X2i8tejBcL%Ojy!wHth^?Cn6qty-=??(T=WcJ+)Ro9j1 z(0hbvl@0*B%14ev9${8d`SwHga)C&C*k!-fe-gS@lWC`3dlewd zOqHbEmm?kBES@F@75zV=Z-PcCjr8nA0;|N(0y|tq;f6@jK2z5IW#^kK76ux=ILJnm zYjdT70Ky{^{jPz(yG51p9wHv}mD}&8tjy7qM2{ij^Z?tv1i*-}4a$J-q=`xeEfGb9?j11RRVlAtH# zou;+2vi|MG)^UQgL8;(3cr{Kv0W^DWs<0n%qg!5$-PyX2h+|a1XYi%1XG$%$lQ7tq zxWpTF8{E#N#Jg(MiwawSy(1ZLV0H)}5beE;O3LRwdsxw5q}TylN5V(P4PPkR1=BQ) zWz@NzXL8hI8tszTgX}oJUIm< zIf}WT0QvMtN(X6ieF69LQf@fRLqNQuP3uUFK!@YC#m@JKiZ`<`ZGW2}*iyk!N0R7J zX5)!Cu127MH!C_s#@S9SSLQ88pQgBN+9TXXlpBEg8NQ#??sEvwG8an>!|B`nHfWQAx zSHEY(KiQMPB)JKctla7gm9f3?uE*#(auFt1pFr9pZF~S;{64Qbb6p&*auHaCvn74Y zNq<>Onmz+_J9+s@~?$X zn6to&rjEvUWtl=fCsI68qXbINPX_%*q^gY165odf8(OuTJ*C5G;ojB4f;8S^hcs{> z%xvG3N#D1~Swgutgqd?HQYc`|qC z9(H+q>J4Jxy8dYZ_Ql*=4%p!nv5c(rxk#Q!H&)PNe~(n<74QJqo=<8>d`bjE6=qUd zTRMB)`S^Vt(E~_$3fsR^J<01h*6U#vd@872@4<8og71^^|9Q(&3v?|4rT)7>=J(3E zi4od$-Q-iC;)gLI3crL(*)xBh{O3LCi9z|YlRXke6m`Lg4i|`xxX2jbbDzDKlEzh> z6eVPSZVY9F|7S=Tu2ZbFY^-_84ejSyojY)z&%1~griK8*@jAAA&HS$iyW%|`urV(q zTfg(yE#x?Srg4%!u1V-11yfuGjMds=UdoG-4lWRoSx7h1Upccyc&B*eA(?x(>t3Zl zi|`ioX2SIma84+$61hsL5Z-^)lAZ{dWM{cDl{ZwdEf+JMd}=!jc*N5#-uiL%1*GpX zZR0qUc)u`U{(@m&_y9IH1<@cU*%Qt7-S5lXI%C5e4OT60h;=kV-rq{p_o?!2YPlH( z6+mrIBl)ZhP=ptGio?13i)=M(WQiD}21+j0PX4xW+@&W& zmdPOVHT(7KFk6rem5u_MFFU3p%2p&LGVW`mX~5}rr5nJ3Db!_dJcPF~hiu8(l{OJz zr|0yI#Uc(gsD~JFr@8A^!Du7kBt{KpN2OmCPxm?9U0KudYD@#MpW|?!=pm0!hZPvq zoJk!unB9@6biQKkZxo2+h7YHdvAKix8PHbKC$8{ZGWJ~v_=oX;H|~+70eMy&4hnJB zY6nu!n<=#pxG|$3;FJG77?cNzkPl)O0P=&PZ$AjC{6z^9-k$yMA*anj5hoxwM4s!( z0O1q%b8V1GK#>*p_UV)E&F2TEZkf=UnVk>Dj`1WridQ}Mcc!d zo_8F7GJ(9K0|jJ_zcX%&kGgxrCF!qHC=LLja_H%)Eu2VWORPP`DI-6?3Ib(87nc+& zXuT?4`mUBzA@4uCLZ^57tCmCPEtrT)YA3gvM>>T>^38>fd4x&zv38#`O&$%Oxe6j< z=V>-21&xmfr!fkM4Gh>jp@;jTx?~E_%kmPwYR{TURBkjVQr4$pSh&FbkdVer-r}e| zd!*xK)-jz{My%%jf(4;rPzB`+HK2XS$E4dOPjmtHeK;WhaM>I^l9J@NJd$3wge#1* z(yTc}>`4{rdB2Msgjc-Vx%}pxq51{Ai=4m5lSyxAqp4vQ2Lg9>_r*@d-WmGbmVmUA z-kl2trwSKhmtU;C<^iAhmjTJ zr$~c!EFkb8HDvoT^sTpR>5V#dw47c^E7PT=h-&Szgd|8WAKJkAAJ;8WpV9S4UVxc*;!&36s6|MJ+$q2+E`dylf=V|Th7bm+V>c$^FH9k`jzNO6)o=cE*_U({rM}tS(lg20Zh(np(eJ! z+bgLFkw1AmWPv4AKNx>A0%K}8z0s_}>7yzsIAcI?syI?$sXqZg+lc`_Qpv$KO*dRoVy6impm4KtXjS%b?6LXhg_Q!Jx#lnmFOwfHCZX z?ClSJ%e{<+DQBBo7z;8%SHdH*MCSUql%<`-ng-GPu2&M9`P%hYj46D4g2Rw(3$bG1 z`MVxCXpSe>;erPjWyhhn6=e02rEOnmRfy6Xz|uE-VlD-{t(6phV2kovI(Wfp5Ewb2 z8eX1+9{Zxr0ciiGKXYMbGl32Ge0aThwh`+}a+Zm$-~8_V6U37jWcN~EsGsF%#}yeqvFy!Sl3&%& z(c)?YvW&}B)0N0R+}fhEe?f28#X4}wm!e2q9ac@wxcGMi$mliYut%iXu9>)>CXgg2AKtRsW+trkXn~k*8{}5m^e_9x@ESSy-9bEVU%e|ekyr3GO83a>dg86}!cy2@u)AMJnFm9;?CW0$gB z8YKo3>F>d3&;0a%1%Mzp+NscN6*mME2LOTbc(J{4v#>AUbAry;Nw2?eLfz&9f35)7 zJ!fI(u1=o!`J!YRHCl+ zuFJX>>_@;xVnVnN=S`c)R9rggVS%`M79a8HLnLT)>eW!US^n!C@XUPdR`FO~#G8pw zldx;#zpfG@LA$;j-m65-VOFC4ARE{aJpY=XRBD!gIHxzC;&4$MLFE1)+NTki#l{@z z(cryo-uCXrk!B0bHXShjD%Ibaf5(lg_v|FBYLtY*SO`pzsSwBV{lx5ye9l@9E(b_$ zwWGM&x~RDKiZyW)R>=YQR)!)v-tOU}`c6#A1^_WiHhgTUhV}w^3>snLTsw*dAkxTndZ&!ml4Be9q5h z`D#5u-UAzXbdA~m(F3b3>xcox^!H{GvQst0eMTNWrIAV&5_)z`t;mwOl7}Pm2X7(##{sG&A_e}9jPF3+6ix>OnGt%Y zxx1`Ll7^q=ZC;_mm_QiXmeLSP|2i=ju_*Md5@9+sU|CB{$UImuD+otOo9SZff_{e=`r;w^j;}1Ym*Z zn8s+K-+ThXJI)WwKaO6xU%Ma_gWRXYot+ts%tt?OUd*WoH{MoqcpEbssq9XHV&DSs z|F}6w>SpF40)|W9_io}z#rUMsU<}}+)`nTf8F5-w*20A6QxcDTVWUdE_Z>4^qauY8-yE#;s6a zM!@q?cCTchta4}@9a|iJF2jvH24<@=v53uAMaasW|IL7>`8nJFuA==$=(oUscp?7L zJ*&erP_D$u!rpgf4?3?C7jpxjgOl>eA%|mu3*QoiexH@BXMF{37g{DvfvsEwG=>Fm zS_T7(ccmVQ8m+!W7)HoU2GOqp|a&vb4dHi{!HEz&Li`XPR1z$i51F=8xiPRPqR&ke{ zQ*rUY=s(#QbKe1gJPX2-a&jN+9-b!JaX+Qh!#$mz_VqmdG#X z>{gI$Z`J$=#oWrfTBlIYvZ|HFeNUpB*KT~2q2K*hr2C_ra&OsU&b=}&&CVNCmN=ev zs`N7h)jHY6<6FufYR*x8jZ~$XX^ou(D-o zXlq7x#)*b}WN+A0VQVo)wq)IBVtPTC0kHW4n615U?r>U;4D5KU)S?sgB-F9rf{^#< z#~`{W4gYAXl%=ZFpe{DnTS|M+-q-s;K<~)qb0U55QCw2W-S57(wjQ`DX>$o9k3r*| z*>vmQE6M95+uv%UMFjzaJ`x#u-JG_mfq%Qiw%7c43s1B&KS~NpFJ4@}?wqoCJ)}qU z#Bb)HtLANqO2+}ky$N}()g9w!hcFw9dF?r?N_a=%1O=*RDm9BO%UYpV{Gc~xU3ZW0 z34YY#Dg%2;?=|s5dfM63_OfOEBCFk$PLP^eOQ`8e%}^``3{eNqy$nENq~LHgO|?RY zZd#ac1x1ByWv;M-~GNK6ANPx)k z-KG0YD3h9__O}_ncX?IB#qFWpi3VQiHFF30e~P&Fc&6Jn{@a+tL@RR+lS5|a6qTIj zkP>>F50UMuG$JA87`C!{9D0;mByt)(WD^?7VUiRvk}zo^Qg05C3CVlwc|V_b|Lya; zeuwLP-S>TcFICZWNAq*D%a%X3s8~}z$c;e;KP|3$h{P{LpaX$7vGbOERw1bKbz7=o zy=iuV*K%=|iXS|36cGZ6aA>T(ibz`J9VqS)q`wy*Pz4LIh7!QTsupPghb!2xL~U8D z7BpQnOFQUFiUCU6!b&PBB0hSXGt_^HNN@ocZ(Top?WGH-#u5CL$M8D1o8Z-WqgYwz z8*NTB6eHQ_jUlp}qz5|Gs)xE{KW%o8PUnJ#48BIDC@d0TE~!bXx1*cI4JSuE}%IykZP1+4Ed8thM_mMw%rB zfQOhCo+;5NMuf_5w7l^8$K0_8AuxZ2bJ}J3{AV$4m!nxoQ#stYrmW$)l=jy1tv8`kFM&A2z?U z*-Z(F@y%~+BVW*6%kG;Vel?Q4b67=7MhI3hYYw&JQOCXpIyPqP5wUQLPu&4fB8ab* z{DrRDumxe^YquXcp4?RCE`ZNVV)Q0$=R`Z34Yt(88Y7+pLqRpiYN#JI>7W&CnTB(9 zN2OnCGm@FsPo?giXTGJB65Gl(mD?`mj~Q#v>_(SCE+ z-E+PI(?q*&TG=#7d*8BXO}UQ*?s1xI)6-RM#+z`5P&Xcaxvuo z#(?<``_;Nspf)NHFyT6TUTVH1@S0Mv_yu(k1JhYvXkjJzJ1fV zBk(gx*50i6A0KCw}v5w;a-*1Vnus(=3NGA&z2w&MY`aI&QWn} zU5@f3S3xosnHmVfmy(+gt&lJ-tjk|ff+W|W?vhDSb0O=!4-x7_}} zXA4?#MSk*j&qVx%%S31%20^TdXIgb!aPcqk!hC7@J45uV2 zGWmQhTVLU}LU5x9DUZzS7pdO@8aNOFu(3^k2rWXo3r=fVPhBwO!1;=JIhhN*4arj+ z_59!@)87I68Pdn(layGq$g~bux7#VSP*+y~NRRWM7UMVzUFRTQW>K_v4a4m3Fq3>| z81m~{4)u5zP%t<_c9jAA4fI^TdE4{cER965N%gF$H~!ya(Wf2tJPq@O=kJhtyR_IY zva<1jxrb*z^vRG0I&l~yC7s;>Vfe7vFN5r!46jta<&Z=BWuA#Lw)}Euk?ocOcij|U zJM(h3d-GH%x)uGowd|cf&+6Yqa2;Mc_w#)aW=LY}veWZ=(qwuASEJfRR_6b2S()TR zMu)GpVrOLpxd-bMxup;S4nhZjd1J*+#rlpgfH`Udb4)!ZZdmr-0`f?jojgpY$Xv0m{La4L;9jG?Q2Qv{<5)j? zJJCQQodNXe!#)#!qbFK751;WINtpfiYMk2Yd#eJ}40K|%ROUfwcsi$j5o~LKmQ3jC zac`T>V15N?sE)<6rT;Z@1X9_N)oXu38O@3_UHkY7)&Kh`5Ypryq8UWBTKj@eX=LITWOEf(`tXPeXPTQkz7nC9%WCOwbnt2_sOpe zrM-^*Dr+76UH4qlAPwBTM+m2v`waL$+<&Mm5*&fu0c=|rr}J%^;vq;sw$uWfIK2A= z(#{WLge0N)U8r~Cfit4TdH5Q~VnAwqL75QN-KMG-7}cW5i5sldcg6~Jm2xu0eVP{y zlK+EPNDh&0F*-@9H$aF0-I9{SBdqW(ryR26pVuJGkDAPwMzF4|xMNPkSK?MGiWZh3 zaQ;X>fVY~m1?t5%i9C4ho?Il^yJlV!b?~r)&mIKL2~6dfLe}o=CHvPzwPqs5pQ7ap zz_-Ajwyzs^vEE5dH@W1@O{S(z1Z-U_BfJrQa5Qkg-5`AEOOD`RFyX8SI% z(^HKb4*!Y<2}H9SO47*8Vfy-;l(g3GL#zUuD7Z&*Fiqg9K&#bcx|SA0>n+GF;-GxX zLN$2E?Dae+D7rt{k??vmd!(%E*IF3ZcchONrb-UnU0*?OuR4jmypyytIhhPv#n-{q zq7ZFsh0E}Zz-fCA-q%p}@|kgZyCvV7bYP&il_X#gzi@GEd!Md`)!HzQBz z0qV@BAq8Xl!JhInDBtP8v^qv+vZgx*4YvJqK=am~k-7L%3zM4#TWlzn!0M;fJ#I+V ze7V&^`(kCYqz{H(gWST~FNGNr zA_(V>K3M;TrgLv#4ZgxtI&S9`j#@bLI1RL*ta4*(gV};-9{gl^C67na3Ns&%P}|sumtMRt8E%i!-+hm>`<3R%5k#Y#RnIZNbziAlA#S}DMqKF+ai@|Va z#|9CoVT?kt^x2KX+h!qenTKSbl^j7Ce6AGIhkh{0yc*H7flRQV+d?Nq zWbIy9stoFpZ)HiAh^LP27ACilFSeIjsOrAyaU~2!JFP9Xc2fSMK?E!<#3#6`vnzM!p^Dh3rWu?GT8YdNU4^h*PZ?~lpC-S znSeVrWeaSNbU#I%-Rv11MjKaxHw>GkZR~jXJzENtfd?r$%3YSHj4$l9&EOK^u29Cf z5u*2)BUJ9Tg1%|D0p^!mi-|)Wld(QX;8G<8FGcA=D?PT*R7I+1w@d{VcW=@S^AJuv zy2k35`5ODg*P~R$0ChL!P^+2oj~$f5Ww6grdnPV%w|&=;O-WI91srwiWm1WW{%ZYi zIOGbvAgScHggwO3KNk>^hiVG<5gE&Zw*IX`^FmM_az+uK!IT-c;^*s>pyj$?n80rC z;9cU?u_)^k)R3N$JJk!mdI&ds@0uFvK#ii24DUxe!7^4p^B(kC;T`skK{``FS4zBf zx>i_@3&XT_TlfdJszBwB8Fz3I#D$wn`IByAE|lRA`Zc~LPnRY_^oUYZ+$Tr9E~--2 zi?=sGi9M!Tg{7^lAJ(!3UvIQJ^ht*bgE(8w`V@2}kDj|CArmAL-l-NlA4}S&xLeFnF8_Rk)7^cBF-Ky!6IC1abN8EA z8lq|ly=`p5qIlU;^N8(9L2Wd$Ie<`+)@GxtVmoqU0y{1YH%`>W%#89i3(BMDRr3$J z3=3dmdIOi5hjsE2zE!RsX^xlXPZ^#=+8r9J;A|u6+G~53MqVY%n`3rfxoG&JbRiPd z^Z>IChE4THP-T{_dAgQ7Ye$S5v;mw)+a`O+M1=YLA<}Khd7X8C&<<*q? TIUJ)80zZcXN9>q3B { uiState: false, }); - domNode.style.width = '256px'; - domNode.style.height = '368px'; await tagcloudVisualization.render(dummyTableGroup, vis.params, { resize: true, params: false, diff --git a/src/plugins/vis_type_vega/public/__mocks__/services.ts b/src/plugins/vis_type_vega/public/__mocks__/services.ts deleted file mode 100644 index 4775241a66d50..0000000000000 --- a/src/plugins/vis_type_vega/public/__mocks__/services.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { CoreStart, IUiSettingsClient, NotificationsStart, SavedObjectsStart } from 'kibana/public'; - -import { createGetterSetter } from '../../../kibana_utils/public'; -import { DataPublicPluginStart } from '../../../data/public'; -import { dataPluginMock } from '../../../data/public/mocks'; -import { coreMock } from '../../../../core/public/mocks'; - -export const [getData, setData] = createGetterSetter('Data'); -setData(dataPluginMock.createStartContract()); - -export const [getNotifications, setNotifications] = createGetterSetter( - 'Notifications' -); -setNotifications(coreMock.createStart().notifications); - -export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); -setUISettings(coreMock.createStart().uiSettings); - -export const [getInjectedMetadata, setInjectedMetadata] = createGetterSetter< - CoreStart['injectedMetadata'] ->('InjectedMetadata'); -setInjectedMetadata(coreMock.createStart().injectedMetadata); - -export const [getSavedObjects, setSavedObjects] = createGetterSetter( - 'SavedObjects' -); -setSavedObjects(coreMock.createStart().savedObjects); - -export const [getInjectedVars, setInjectedVars] = createGetterSetter<{ - enableExternalUrls: boolean; - emsTileLayerId: unknown; -}>('InjectedVars'); -setInjectedVars({ - emsTileLayerId: {}, - enableExternalUrls: true, -}); - -export const getEnableExternalUrls = () => getInjectedVars().enableExternalUrls; -export const getEmsTileLayerId = () => getInjectedVars().emsTileLayerId; diff --git a/src/plugins/vis_type_vega/public/__snapshots__/vega_visualization.test.js.snap b/src/plugins/vis_type_vega/public/__snapshots__/vega_visualization.test.js.snap new file mode 100644 index 0000000000000..650d9c1b430f0 --- /dev/null +++ b/src/plugins/vis_type_vega/public/__snapshots__/vega_visualization.test.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`VegaVisualizations VegaVisualization - basics should show vega blank rectangle on top of a map (vegamap) 1`] = `"
"`; + +exports[`VegaVisualizations VegaVisualization - basics should show vega graph (may fail in dev env) 1`] = `"
"`; + +exports[`VegaVisualizations VegaVisualization - basics should show vegalite graph and update on resize (may fail in dev env) 1`] = `"
"`; + +exports[`VegaVisualizations VegaVisualization - basics should show vegalite graph and update on resize (may fail in dev env) 2`] = `"
"`; diff --git a/src/plugins/vis_type_vega/public/default_spec.ts b/src/plugins/vis_type_vega/public/default_spec.ts new file mode 100644 index 0000000000000..71f44b694a10e --- /dev/null +++ b/src/plugins/vis_type_vega/public/default_spec.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// @ts-ignore +import defaultSpec from '!!raw-loader!./default.spec.hjson'; + +export const getDefaultSpec = () => defaultSpec; diff --git a/src/plugins/vis_type_vega/public/test_utils/default.spec.json b/src/plugins/vis_type_vega/public/test_utils/default.spec.json new file mode 100644 index 0000000000000..8cf763647115f --- /dev/null +++ b/src/plugins/vis_type_vega/public/test_utils/default.spec.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v4.json", + "title": "Event counts from all indexes", + "data": { + "url": { + "%context%": true, + "%timefield%": "@timestamp", + "index": "_all", + "body": { + "aggs": { + "time_buckets": { + "date_histogram": { + "field": "@timestamp", + "interval": { "%autointerval%": true }, + "extended_bounds": { + "min": { "%timefilter%": "min" }, + "max": { "%timefilter%": "max" } + }, + "min_doc_count": 0 + } + } + }, + "size": 0 + } + }, + "format": { "property": "aggregations.time_buckets.buckets" } + }, + "mark": "line", + "encoding": { + "x": { + "field": "key", + "type": "temporal", + "axis": { "title": false } + }, + "y": { + "field": "doc_count", + "type": "quantitative", + "axis": { "title": "Document count" } + } + } +} diff --git a/src/plugins/vis_type_vega/public/test_utils/vega_graph.json b/src/plugins/vis_type_vega/public/test_utils/vega_graph.json new file mode 100644 index 0000000000000..babde96fd3dae --- /dev/null +++ b/src/plugins/vis_type_vega/public/test_utils/vega_graph.json @@ -0,0 +1,78 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "data": [ + { + "name": "table", + "values": [ + {"x": 0, "y": 28, "c": 0}, {"x": 0, "y": 55, "c": 1}, {"x": 1, "y": 43, "c": 0}, {"x": 1, "y": 91, "c": 1}, + {"x": 2, "y": 81, "c": 0}, {"x": 2, "y": 53, "c": 1}, {"x": 3, "y": 19, "c": 0}, {"x": 3, "y": 87, "c": 1}, + {"x": 4, "y": 52, "c": 0}, {"x": 4, "y": 48, "c": 1}, {"x": 5, "y": 24, "c": 0}, {"x": 5, "y": 49, "c": 1}, + {"x": 6, "y": 87, "c": 0}, {"x": 6, "y": 66, "c": 1}, {"x": 7, "y": 17, "c": 0}, {"x": 7, "y": 27, "c": 1}, + {"x": 8, "y": 68, "c": 0}, {"x": 8, "y": 16, "c": 1}, {"x": 9, "y": 49, "c": 0}, {"x": 9, "y": 15, "c": 1} + ], + "transform": [ + { + "type": "stack", + "groupby": ["x"], + "sort": {"field": "c"}, + "field": "y" + } + ] + } + ], + "scales": [ + { + "name": "x", + "type": "point", + "range": "width", + "domain": {"data": "table", "field": "x"} + }, + { + "name": "y", + "type": "linear", + "range": "height", + "nice": true, + "zero": true, + "domain": {"data": "table", "field": "y1"} + }, + { + "name": "color", + "type": "ordinal", + "range": "category", + "domain": {"data": "table", "field": "c"} + } + ], + "marks": [ + { + "type": "group", + "from": { + "facet": {"name": "series", "data": "table", "groupby": "c"} + }, + "marks": [ + { + "type": "area", + "from": {"data": "series"}, + "encode": { + "enter": { + "interpolate": {"value": "monotone"}, + "x": {"scale": "x", "field": "x"}, + "y": {"scale": "y", "field": "y0"}, + "y2": {"scale": "y", "field": "y1"}, + "fill": {"scale": "color", "field": "c"} + }, + "update": { + "fillOpacity": {"value": 1} + }, + "hover": { + "fillOpacity": {"value": 0.5} + } + } + } + ] + } + ], + "autosize": { "type": "none" }, + "width": 512, + "height": 512, + "config": { "kibana": { "renderer": "svg" }} +} diff --git a/src/plugins/vis_type_vega/public/test_utils/vega_map_test.json b/src/plugins/vis_type_vega/public/test_utils/vega_map_test.json new file mode 100644 index 0000000000000..9100de38ae387 --- /dev/null +++ b/src/plugins/vis_type_vega/public/test_utils/vega_map_test.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "config": { + "kibana": { "renderer": "svg", "type": "map", "mapStyle": false} + }, + "width": 512, + "height": 512, + "marks": [ + { + "type": "rect", + "encode": { + "enter": { + "fill": {"value": "#0f0"}, + "width": {"signal": "width"}, + "height": {"signal": "height"} + } + } + } + ] +} diff --git a/src/plugins/vis_type_vega/public/test_utils/vegalite_graph.json b/src/plugins/vis_type_vega/public/test_utils/vegalite_graph.json new file mode 100644 index 0000000000000..5394f009b074f --- /dev/null +++ b/src/plugins/vis_type_vega/public/test_utils/vegalite_graph.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v4.json", + "data": { + "format": {"property": "aggregations.time_buckets.buckets"}, + "values": { + "aggregations": { + "time_buckets": { + "buckets": [ + {"key": 1512950400000, "doc_count": 0}, + {"key": 1513036800000, "doc_count": 0}, + {"key": 1513123200000, "doc_count": 0}, + {"key": 1513209600000, "doc_count": 4545}, + {"key": 1513296000000, "doc_count": 4667}, + {"key": 1513382400000, "doc_count": 4660}, + {"key": 1513468800000, "doc_count": 133}, + {"key": 1513555200000, "doc_count": 0}, + {"key": 1513641600000, "doc_count": 0}, + {"key": 1513728000000, "doc_count": 0} + ] + } + }, + "status": 200 + } + }, + "mark": "line", + "encoding": { + "x": { + "field": "key", + "type": "temporal", + "axis": null + }, + "y": { + "field": "doc_count", + "type": "quantitative", + "axis": null + } + }, + "autosize": { "type": "fit" }, + "width": 512, + "height": 512, + "config": { "kibana": { "renderer": "svg" }} +} diff --git a/src/plugins/vis_type_vega/public/vega_type.ts b/src/plugins/vis_type_vega/public/vega_type.ts index 55ad134c05301..5825661f9001c 100644 --- a/src/plugins/vis_type_vega/public/vega_type.ts +++ b/src/plugins/vis_type_vega/public/vega_type.ts @@ -25,8 +25,7 @@ import { VegaVisEditor } from './components'; import { createVegaRequestHandler } from './vega_request_handler'; // @ts-ignore import { createVegaVisualization } from './vega_visualization'; -// @ts-ignore -import defaultSpec from '!!raw-loader!./default.spec.hjson'; +import { getDefaultSpec } from './default_spec'; export const createVegaTypeDefinition = (dependencies: VegaVisualizationDependencies) => { const requestHandler = createVegaRequestHandler(dependencies); @@ -40,7 +39,7 @@ export const createVegaTypeDefinition = (dependencies: VegaVisualizationDependen description: 'Vega and Vega-Lite are product names and should not be translated', }), icon: 'visVega', - visConfig: { defaults: { spec: defaultSpec } }, + visConfig: { defaults: { spec: getDefaultSpec() } }, editorConfig: { optionsTemplate: VegaVisEditor, enableAutoApply: true, diff --git a/src/plugins/vis_type_vega/public/vega_visualization.test.js b/src/plugins/vis_type_vega/public/vega_visualization.test.js new file mode 100644 index 0000000000000..a6ad6e4908bb4 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_visualization.test.js @@ -0,0 +1,232 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import $ from 'jquery'; + +import 'leaflet/dist/leaflet.js'; +import 'leaflet-vega'; +import { createVegaVisualization } from './vega_visualization'; + +import vegaliteGraph from './test_utils/vegalite_graph.json'; +import vegaGraph from './test_utils/vega_graph.json'; +import vegaMapGraph from './test_utils/vega_map_test.json'; + +import { VegaParser } from './data_model/vega_parser'; +import { SearchAPI } from './data_model/search_api'; + +import { createVegaTypeDefinition } from './vega_type'; + +import { + setInjectedVars, + setData, + setSavedObjects, + setNotifications, + setKibanaMapFactory, +} from './services'; +import { coreMock } from '../../../core/public/mocks'; +import { dataPluginMock } from '../../data/public/mocks'; +import { KibanaMap } from '../../maps_legacy/public/map/kibana_map'; + +jest.mock('./default_spec', () => ({ + getDefaultSpec: () => jest.requireActual('./test_utils/default.spec.json'), +})); + +jest.mock('./lib/vega', () => ({ + vega: jest.requireActual('vega'), + vegaLite: jest.requireActual('vega-lite'), +})); + +describe('VegaVisualizations', () => { + let domNode; + let VegaVisualization; + let vis; + let vegaVisualizationDependencies; + let vegaVisType; + + let mockWidth; + let mockedWidthValue; + let mockHeight; + let mockedHeightValue; + + const coreStart = coreMock.createStart(); + const dataPluginStart = dataPluginMock.createStartContract(); + + const setupDOM = (width = 512, height = 512) => { + mockedWidthValue = width; + mockedHeightValue = height; + domNode = document.createElement('div'); + + mockWidth = jest.spyOn($.prototype, 'width').mockImplementation(() => mockedWidthValue); + mockHeight = jest.spyOn($.prototype, 'height').mockImplementation(() => mockedHeightValue); + }; + + setKibanaMapFactory((...args) => new KibanaMap(...args)); + setInjectedVars({ + emsTileLayerId: {}, + enableExternalUrls: true, + esShardTimeout: 10000, + }); + setData(dataPluginStart); + setSavedObjects(coreStart.savedObjects); + setNotifications(coreStart.notifications); + + beforeEach(() => { + vegaVisualizationDependencies = { + core: coreMock.createSetup(), + plugins: { + data: dataPluginMock.createSetupContract(), + }, + }; + + vegaVisType = createVegaTypeDefinition(vegaVisualizationDependencies); + VegaVisualization = createVegaVisualization(vegaVisualizationDependencies); + }); + + describe('VegaVisualization - basics', () => { + beforeEach(async () => { + setupDOM(); + + vis = { + type: vegaVisType, + }; + }); + + afterEach(() => { + mockWidth.mockRestore(); + mockHeight.mockRestore(); + }); + + test('should show vegalite graph and update on resize (may fail in dev env)', async () => { + let vegaVis; + try { + vegaVis = new VegaVisualization(domNode, vis); + + const vegaParser = new VegaParser( + JSON.stringify(vegaliteGraph), + new SearchAPI({ + search: dataPluginStart.search, + uiSettings: coreStart.uiSettings, + injectedMetadata: coreStart.injectedMetadata, + }) + ); + await vegaParser.parseAsync(); + await vegaVis.render(vegaParser); + expect(domNode.innerHTML).toMatchSnapshot(); + + mockedWidthValue = 256; + mockedHeightValue = 256; + + await vegaVis._vegaView.resize(); + + expect(domNode.innerHTML).toMatchSnapshot(); + } finally { + vegaVis.destroy(); + } + }); + + test('should show vega graph (may fail in dev env)', async () => { + let vegaVis; + try { + vegaVis = new VegaVisualization(domNode, vis); + const vegaParser = new VegaParser( + JSON.stringify(vegaGraph), + new SearchAPI({ + search: dataPluginStart.search, + uiSettings: coreStart.uiSettings, + injectedMetadata: coreStart.injectedMetadata, + }) + ); + await vegaParser.parseAsync(); + + await vegaVis.render(vegaParser); + expect(domNode.innerHTML).toMatchSnapshot(); + } finally { + vegaVis.destroy(); + } + }); + + test('should show vega blank rectangle on top of a map (vegamap)', async () => { + let vegaVis; + try { + vegaVis = new VegaVisualization(domNode, vis); + const vegaParser = new VegaParser( + JSON.stringify(vegaMapGraph), + new SearchAPI({ + search: dataPluginStart.search, + uiSettings: coreStart.uiSettings, + injectedMetadata: coreStart.injectedMetadata, + }) + ); + await vegaParser.parseAsync(); + + mockedWidthValue = 256; + mockedHeightValue = 256; + + await vegaVis.render(vegaParser); + expect(domNode.innerHTML).toMatchSnapshot(); + } finally { + vegaVis.destroy(); + } + }); + + test('should add a small subpixel value to the height of the canvas to avoid getting it set to 0', async () => { + let vegaVis; + try { + vegaVis = new VegaVisualization(domNode, vis); + const vegaParser = new VegaParser( + `{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "marks": [ + { + "type": "text", + "encode": { + "update": { + "text": { + "value": "Test" + }, + "align": {"value": "center"}, + "baseline": {"value": "middle"}, + "xc": {"signal": "width/2"}, + "yc": {"signal": "height/2"} + fontSize: {value: "14"} + } + } + } + ] + }`, + new SearchAPI({ + search: dataPluginStart.search, + uiSettings: coreStart.uiSettings, + injectedMetadata: coreStart.injectedMetadata, + }) + ); + await vegaParser.parseAsync(); + + mockedWidthValue = 256; + mockedHeightValue = 256; + + await vegaVis.render(vegaParser); + const vegaView = vegaVis._vegaView._view; + expect(vegaView.height()).toBe(250.00000001); + } finally { + vegaVis.destroy(); + } + }); + }); +}); From 54e09cd94e13b5b567705cee39d2edfe41f310cc Mon Sep 17 00:00:00 2001 From: Nick Peihl Date: Tue, 14 Jul 2020 08:26:58 -0700 Subject: [PATCH 03/82] Update ems-landing-page-url to 7.9 (#71532) --- src/legacy/server/config/schema.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index 53f5185442688..952c35df244c1 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -237,7 +237,7 @@ export default () => manifestServiceUrl: Joi.string().default('').allow(''), emsFileApiUrl: Joi.string().default('https://vector.maps.elastic.co'), emsTileApiUrl: Joi.string().default('https://tiles.maps.elastic.co'), - emsLandingPageUrl: Joi.string().default('https://maps.elastic.co/v7.8'), + emsLandingPageUrl: Joi.string().default('https://maps.elastic.co/v7.9'), emsFontLibraryUrl: Joi.string().default( 'https://tiles.maps.elastic.co/fonts/{fontstack}/{range}.pbf' ), From 30cc39855dd103699aef86d67363057f38b2da0d Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Tue, 14 Jul 2020 11:35:32 -0400 Subject: [PATCH 04/82] Ignore equality check against the `manifest_version` of the full agent config (#71637) --- .../apps/endpoint/policy_details.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index 9a0a819f68b62..cf76f297d83be 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -155,7 +155,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', }, }, - manifest_version: 'WzEwNSwxXQ==', + // The manifest version could have changed when the Policy was updated because the + // policy details page ensures that a save action applies the udpated policy on top + // of the latest Package Config. So we just ignore the check against this value by + // forcing it to be the same as the value returned in the full agent config. + manifest_version: agentFullConfig.inputs[0].artifact_manifest.manifest_version, schema_version: 'v1', }, policy: { From d8823d899682a2439b90676ae09b95a5079d83f3 Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Tue, 14 Jul 2020 18:41:12 +0300 Subject: [PATCH 05/82] Migrate timelion to the NP. (#69160) * Migrate timelion to the NP. * fixed ci * Fixed paths * fixed UI settings * Fixed ci * fix CI * Fixed some comments * Fixed browser tests * fixed state * Fixed comments * Fixed save expression * Fixed navigation * fix CI * Fixed some problem Co-authored-by: Elastic Machine Co-authored-by: Alexey Antonov --- .eslintignore | 1 + .i18nrc.json | 2 +- .sass-lint.yml | 2 +- src/legacy/core_plugins/timelion/index.ts | 189 - src/legacy/core_plugins/timelion/package.json | 5 - .../core_plugins/timelion/public/app.js | 517 --- .../timelion/public/directives/cells/cells.js | 53 - .../public/directives/fixed_element.js | 49 - .../public/directives/saved_object_finder.js | 315 -- .../directives/timelion_expression_input.js | 281 -- .../public/directives/timelion_grid.js | 67 - .../directives/timelion_help/timelion_help.js | 168 - .../core_plugins/timelion/public/header.svg | 227 -- .../core_plugins/timelion/public/icon.svg | 97 - .../core_plugins/timelion/public/legacy.ts | 35 - .../core_plugins/timelion/public/logo.png | Bin 14638 -> 0 bytes .../core_plugins/timelion/public/plugin.ts | 74 - .../timelion/public/services/saved_sheets.ts | 52 - .../public/shim/timelion_legacy_module.ts | 55 - .../state_management/__tests__/state.js | 3 + src/plugins/saved_objects/public/index.ts | 1 + src/plugins/timelion/kibana.json | 21 +- .../timelion/public/_app.scss | 0 src/plugins/timelion/public/app.js | 661 ++++ src/plugins/timelion/public/application.ts | 153 + .../timelion/public/breadcrumbs.js | 0 .../public/components/timelionhelp_tabs.js | 4 +- .../timelionhelp_tabs_directive.js} | 42 +- .../timelion/public/directives/_index.scss | 0 .../_timelion_expression_input.scss | 0 .../public/directives/cells/_cells.scss | 0 .../public/directives/cells/_index.scss | 0 .../public/directives/cells/cells.html | 0 .../public/directives/cells/cells.js} | 49 +- .../public/directives/cells/collection.ts | 76 + .../timelion/public/directives/chart/chart.js | 0 .../public/directives/fixed_element.js | 50 + .../directives/fullscreen/fullscreen.html | 2 +- .../directives/fullscreen/fullscreen.js} | 26 +- .../timelion/public/directives/input_focus.js | 35 + .../timelion}/public/directives/key_map.ts | 0 .../directives/saved_object_finder.html | 0 .../public/directives/saved_object_finder.js | 314 ++ .../saved_object_save_as_checkbox.html | 0 .../saved_object_save_as_checkbox.js | 23 +- .../directives/timelion_expression_input.html | 0 .../directives/timelion_expression_input.js | 282 ++ .../timelion_expression_input_helpers.js | 0 .../timelion_expression_suggestions.js | 0 .../_index.scss | 0 .../_timelion_expression_suggestions.scss | 0 .../timelion_expression_suggestions.html | 0 .../timelion_expression_suggestions.js | 0 .../public/directives/timelion_grid.js | 68 + .../directives/timelion_help/_index.scss | 0 .../timelion_help/_timelion_help.scss | 0 .../timelion_help/timelion_help.html | 4 +- .../directives/timelion_help/timelion_help.js | 166 + .../directives/timelion_interval/_index.scss | 0 .../timelion_interval/_timelion_interval.scss | 0 .../timelion_interval/timelion_interval.html | 0 .../timelion_interval/timelion_interval.js | 11 +- .../public/directives/timelion_load_sheet.js} | 12 +- .../directives/timelion_options_sheet.js} | 13 +- .../public/directives/timelion_save_sheet.js} | 20 +- .../timelion/public/flot.js} | 18 +- .../timelion/public/index.html | 10 +- .../timelion/public/index.scss | 0 .../timelion/public/index.ts | 0 .../timelion/public/lib/observe_resize.js | 0 .../timelion/public/panels/panel.ts | 0 .../public/panels/timechart/schema.ts | 31 +- .../public/panels/timechart/timechart.ts | 2 +- .../timelion/public/partials/load_sheet.html | 0 .../timelion/public/partials/save_sheet.html | 0 .../public/partials/sheet_options.html | 0 src/plugins/timelion/public/plugin.ts | 134 + .../timelion/public/services/_saved_sheet.ts | 5 +- .../timelion/public/services/saved_sheets.ts | 50 + .../timelion/public/timelion_app_state.ts | 73 + src/plugins/timelion/public/types.ts | 35 + .../webpackShims/jquery.flot.axislabels.js | 462 +++ .../webpackShims/jquery.flot.crosshair.js | 176 + .../public/webpackShims/jquery.flot.js | 3168 +++++++++++++++++ .../webpackShims/jquery.flot.selection.js | 360 ++ .../public/webpackShims/jquery.flot.stack.js | 188 + .../public/webpackShims/jquery.flot.symbol.js | 71 + .../public/webpackShims/jquery.flot.time.js | 432 +++ .../timelion/server/config.ts} | 20 +- src/plugins/timelion/server/index.ts | 10 +- src/plugins/timelion/server/plugin.ts | 49 +- src/plugins/vis_type_timelion/public/index.ts | 5 + .../vis_type_timelion/server/plugin.ts | 97 +- src/test_utils/public/key_map.ts | 121 + src/test_utils/public/simulate_keys.js | 2 +- 95 files changed, 7419 insertions(+), 2325 deletions(-) delete mode 100644 src/legacy/core_plugins/timelion/index.ts delete mode 100644 src/legacy/core_plugins/timelion/package.json delete mode 100644 src/legacy/core_plugins/timelion/public/app.js delete mode 100644 src/legacy/core_plugins/timelion/public/directives/cells/cells.js delete mode 100644 src/legacy/core_plugins/timelion/public/directives/fixed_element.js delete mode 100644 src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js delete mode 100644 src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js delete mode 100644 src/legacy/core_plugins/timelion/public/directives/timelion_grid.js delete mode 100644 src/legacy/core_plugins/timelion/public/directives/timelion_help/timelion_help.js delete mode 100644 src/legacy/core_plugins/timelion/public/header.svg delete mode 100644 src/legacy/core_plugins/timelion/public/icon.svg delete mode 100644 src/legacy/core_plugins/timelion/public/legacy.ts delete mode 100644 src/legacy/core_plugins/timelion/public/logo.png delete mode 100644 src/legacy/core_plugins/timelion/public/plugin.ts delete mode 100644 src/legacy/core_plugins/timelion/public/services/saved_sheets.ts delete mode 100644 src/legacy/core_plugins/timelion/public/shim/timelion_legacy_module.ts rename src/{legacy/core_plugins => plugins}/timelion/public/_app.scss (100%) create mode 100644 src/plugins/timelion/public/app.js create mode 100644 src/plugins/timelion/public/application.ts rename src/{legacy/core_plugins => plugins}/timelion/public/breadcrumbs.js (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/components/timelionhelp_tabs.js (95%) rename src/{legacy/core_plugins/timelion/public/directives/fullscreen/fullscreen.js => plugins/timelion/public/components/timelionhelp_tabs_directive.js} (56%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/_index.scss (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/_timelion_expression_input.scss (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/cells/_cells.scss (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/cells/_index.scss (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/cells/cells.html (100%) rename src/{legacy/core_plugins/timelion/public/shim/legacy_dependencies_plugin.ts => plugins/timelion/public/directives/cells/cells.js} (50%) create mode 100644 src/plugins/timelion/public/directives/cells/collection.ts rename src/{legacy/core_plugins => plugins}/timelion/public/directives/chart/chart.js (100%) create mode 100644 src/plugins/timelion/public/directives/fixed_element.js rename src/{legacy/core_plugins => plugins}/timelion/public/directives/fullscreen/fullscreen.html (85%) rename src/{legacy/core_plugins/timelion/public/directives/timelion_save_sheet.js => plugins/timelion/public/directives/fullscreen/fullscreen.js} (69%) create mode 100644 src/plugins/timelion/public/directives/input_focus.js rename src/{legacy/ui => plugins/timelion}/public/directives/key_map.ts (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/saved_object_finder.html (100%) create mode 100644 src/plugins/timelion/public/directives/saved_object_finder.js rename src/{legacy/core_plugins => plugins}/timelion/public/directives/saved_object_save_as_checkbox.html (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/saved_object_save_as_checkbox.js (75%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_expression_input.html (100%) create mode 100644 src/plugins/timelion/public/directives/timelion_expression_input.js rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_expression_input_helpers.js (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_expression_suggestions/__tests__/timelion_expression_suggestions.js (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_expression_suggestions/_index.scss (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_expression_suggestions/_timelion_expression_suggestions.scss (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_expression_suggestions/timelion_expression_suggestions.html (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_expression_suggestions/timelion_expression_suggestions.js (100%) create mode 100644 src/plugins/timelion/public/directives/timelion_grid.js rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_help/_index.scss (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_help/_timelion_help.scss (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_help/timelion_help.html (99%) create mode 100644 src/plugins/timelion/public/directives/timelion_help/timelion_help.js rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_interval/_index.scss (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_interval/_timelion_interval.scss (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_interval/timelion_interval.html (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_interval/timelion_interval.js (88%) rename src/{legacy/core_plugins/timelion/public/shim/index.ts => plugins/timelion/public/directives/timelion_load_sheet.js} (76%) rename src/{legacy/core_plugins/timelion/public/services/saved_sheet_register.ts => plugins/timelion/public/directives/timelion_options_sheet.js} (76%) rename src/{legacy/core_plugins/timelion/public/directives/timelion_load_sheet.js => plugins/timelion/public/directives/timelion_save_sheet.js} (74%) rename src/{legacy/core_plugins/timelion/public/directives/timelion_options_sheet.js => plugins/timelion/public/flot.js} (72%) rename src/{legacy/core_plugins => plugins}/timelion/public/index.html (92%) rename src/{legacy/core_plugins => plugins}/timelion/public/index.scss (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/index.ts (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/lib/observe_resize.js (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/panels/panel.ts (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/panels/timechart/schema.ts (93%) rename src/{legacy/core_plugins => plugins}/timelion/public/panels/timechart/timechart.ts (94%) rename src/{legacy/core_plugins => plugins}/timelion/public/partials/load_sheet.html (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/partials/save_sheet.html (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/partials/sheet_options.html (100%) create mode 100644 src/plugins/timelion/public/plugin.ts rename src/{legacy/core_plugins => plugins}/timelion/public/services/_saved_sheet.ts (95%) create mode 100644 src/plugins/timelion/public/services/saved_sheets.ts create mode 100644 src/plugins/timelion/public/timelion_app_state.ts create mode 100644 src/plugins/timelion/public/types.ts create mode 100644 src/plugins/timelion/public/webpackShims/jquery.flot.axislabels.js create mode 100644 src/plugins/timelion/public/webpackShims/jquery.flot.crosshair.js create mode 100644 src/plugins/timelion/public/webpackShims/jquery.flot.js create mode 100644 src/plugins/timelion/public/webpackShims/jquery.flot.selection.js create mode 100644 src/plugins/timelion/public/webpackShims/jquery.flot.stack.js create mode 100644 src/plugins/timelion/public/webpackShims/jquery.flot.symbol.js create mode 100644 src/plugins/timelion/public/webpackShims/jquery.flot.time.js rename src/{legacy/core_plugins/timelion/public/components/timelionhelp_tabs_directive.js => plugins/timelion/server/config.ts} (67%) create mode 100644 src/test_utils/public/key_map.ts diff --git a/.eslintignore b/.eslintignore index 4b5e781c26971..d983c4bedfaab 100644 --- a/.eslintignore +++ b/.eslintignore @@ -26,6 +26,7 @@ target /src/plugins/data/common/es_query/kuery/ast/_generated_/** /src/plugins/vis_type_timelion/public/_generated_/** /src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.* +/src/plugins/timelion/public/webpackShims/jquery.flot.* /x-pack/legacy/plugins/**/__tests__/fixtures/** /x-pack/plugins/apm/e2e/**/snapshots.js /x-pack/plugins/apm/e2e/tmp/* diff --git a/.i18nrc.json b/.i18nrc.json index 9af7f17067b8e..e8431fdb3f0e1 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -44,7 +44,7 @@ "src/plugins/telemetry_management_section" ], "tileMap": "src/plugins/tile_map", - "timelion": ["src/legacy/core_plugins/timelion", "src/plugins/vis_type_timelion"], + "timelion": ["src/plugins/timelion", "src/plugins/vis_type_timelion"], "uiActions": "src/plugins/ui_actions", "visDefaultEditor": "src/plugins/vis_default_editor", "visTypeMarkdown": "src/plugins/vis_type_markdown", diff --git a/.sass-lint.yml b/.sass-lint.yml index 56b85adca8a71..50cbe81cc7da2 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -1,7 +1,7 @@ files: include: - 'src/legacy/core_plugins/metrics/**/*.s+(a|c)ss' - - 'src/legacy/core_plugins/timelion/**/*.s+(a|c)ss' + - 'src/plugins/timelion/**/*.s+(a|c)ss' - 'src/plugins/vis_type_vislib/**/*.s+(a|c)ss' - 'src/plugins/vis_type_xy/**/*.s+(a|c)ss' - 'x-pack/plugins/canvas/**/*.s+(a|c)ss' diff --git a/src/legacy/core_plugins/timelion/index.ts b/src/legacy/core_plugins/timelion/index.ts deleted file mode 100644 index 9c8ab156d1a79..0000000000000 --- a/src/legacy/core_plugins/timelion/index.ts +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { resolve } from 'path'; -import { i18n } from '@kbn/i18n'; -import { Legacy } from 'kibana'; -import { LegacyPluginApi, LegacyPluginInitializer } from 'src/legacy/plugin_discovery/types'; -import { DEFAULT_APP_CATEGORIES } from '../../../core/server'; - -const experimentalLabel = i18n.translate('timelion.uiSettings.experimentalLabel', { - defaultMessage: 'experimental', -}); - -const timelionPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - require: ['kibana', 'elasticsearch'], - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - ui: Joi.object({ - enabled: Joi.boolean().default(false), - }).default(), - graphiteUrls: Joi.array() - .items(Joi.string().uri({ scheme: ['http', 'https'] })) - .default([]), - }).default(); - }, - // @ts-ignore - // https://github.com/elastic/kibana/pull/44039#discussion_r326582255 - uiCapabilities() { - return { - timelion: { - save: true, - }, - }; - }, - publicDir: resolve(__dirname, 'public'), - uiExports: { - app: { - title: 'Timelion', - order: 8000, - icon: 'plugins/timelion/icon.svg', - euiIconType: 'timelionApp', - main: 'plugins/timelion/app', - category: DEFAULT_APP_CATEGORIES.kibana, - }, - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: [resolve(__dirname, 'public/legacy')], - uiSettingDefaults: { - 'timelion:showTutorial': { - name: i18n.translate('timelion.uiSettings.showTutorialLabel', { - defaultMessage: 'Show tutorial', - }), - value: false, - description: i18n.translate('timelion.uiSettings.showTutorialDescription', { - defaultMessage: 'Should I show the tutorial by default when entering the timelion app?', - }), - category: ['timelion'], - }, - 'timelion:es.timefield': { - name: i18n.translate('timelion.uiSettings.timeFieldLabel', { - defaultMessage: 'Time field', - }), - value: '@timestamp', - description: i18n.translate('timelion.uiSettings.timeFieldDescription', { - defaultMessage: 'Default field containing a timestamp when using {esParam}', - values: { esParam: '.es()' }, - }), - category: ['timelion'], - }, - 'timelion:es.default_index': { - name: i18n.translate('timelion.uiSettings.defaultIndexLabel', { - defaultMessage: 'Default index', - }), - value: '_all', - description: i18n.translate('timelion.uiSettings.defaultIndexDescription', { - defaultMessage: 'Default elasticsearch index to search with {esParam}', - values: { esParam: '.es()' }, - }), - category: ['timelion'], - }, - 'timelion:target_buckets': { - name: i18n.translate('timelion.uiSettings.targetBucketsLabel', { - defaultMessage: 'Target buckets', - }), - value: 200, - description: i18n.translate('timelion.uiSettings.targetBucketsDescription', { - defaultMessage: 'The number of buckets to shoot for when using auto intervals', - }), - category: ['timelion'], - }, - 'timelion:max_buckets': { - name: i18n.translate('timelion.uiSettings.maximumBucketsLabel', { - defaultMessage: 'Maximum buckets', - }), - value: 2000, - description: i18n.translate('timelion.uiSettings.maximumBucketsDescription', { - defaultMessage: 'The maximum number of buckets a single datasource can return', - }), - category: ['timelion'], - }, - 'timelion:default_columns': { - name: i18n.translate('timelion.uiSettings.defaultColumnsLabel', { - defaultMessage: 'Default columns', - }), - value: 2, - description: i18n.translate('timelion.uiSettings.defaultColumnsDescription', { - defaultMessage: 'Number of columns on a timelion sheet by default', - }), - category: ['timelion'], - }, - 'timelion:default_rows': { - name: i18n.translate('timelion.uiSettings.defaultRowsLabel', { - defaultMessage: 'Default rows', - }), - value: 2, - description: i18n.translate('timelion.uiSettings.defaultRowsDescription', { - defaultMessage: 'Number of rows on a timelion sheet by default', - }), - category: ['timelion'], - }, - 'timelion:min_interval': { - name: i18n.translate('timelion.uiSettings.minimumIntervalLabel', { - defaultMessage: 'Minimum interval', - }), - value: '1ms', - description: i18n.translate('timelion.uiSettings.minimumIntervalDescription', { - defaultMessage: 'The smallest interval that will be calculated when using "auto"', - description: - '"auto" is a technical value in that context, that should not be translated.', - }), - category: ['timelion'], - }, - 'timelion:graphite.url': { - name: i18n.translate('timelion.uiSettings.graphiteURLLabel', { - defaultMessage: 'Graphite URL', - description: - 'The URL should be in the form of https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite', - }), - value: (server: Legacy.Server) => { - const urls = server.config().get('timelion.graphiteUrls') as string[]; - if (urls.length === 0) { - return null; - } else { - return urls[0]; - } - }, - description: i18n.translate('timelion.uiSettings.graphiteURLDescription', { - defaultMessage: - '{experimentalLabel} The URL of your graphite host', - values: { experimentalLabel: `[${experimentalLabel}]` }, - }), - type: 'select', - options: (server: Legacy.Server) => server.config().get('timelion.graphiteUrls'), - category: ['timelion'], - }, - 'timelion:quandl.key': { - name: i18n.translate('timelion.uiSettings.quandlKeyLabel', { - defaultMessage: 'Quandl key', - }), - value: 'someKeyHere', - description: i18n.translate('timelion.uiSettings.quandlKeyDescription', { - defaultMessage: '{experimentalLabel} Your API key from www.quandl.com', - values: { experimentalLabel: `[${experimentalLabel}]` }, - }), - category: ['timelion'], - }, - }, - }, - }); - -// eslint-disable-next-line import/no-default-export -export default timelionPluginInitializer; diff --git a/src/legacy/core_plugins/timelion/package.json b/src/legacy/core_plugins/timelion/package.json deleted file mode 100644 index 8b138e3b76d1a..0000000000000 --- a/src/legacy/core_plugins/timelion/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "author": "Rashid Khan ", - "name": "timelion", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/timelion/public/app.js b/src/legacy/core_plugins/timelion/public/app.js deleted file mode 100644 index 602b221b7d14d..0000000000000 --- a/src/legacy/core_plugins/timelion/public/app.js +++ /dev/null @@ -1,517 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -// required for `ngSanitize` angular module -import 'angular-sanitize'; - -import { i18n } from '@kbn/i18n'; - -import routes from 'ui/routes'; -import { capabilities } from 'ui/capabilities'; -import { docTitle } from 'ui/doc_title'; -import { fatalError, toastNotifications } from 'ui/notify'; -import { timefilter } from 'ui/timefilter'; -import { npStart } from 'ui/new_platform'; -import { getSavedSheetBreadcrumbs, getCreateBreadcrumbs } from './breadcrumbs'; -import { getTimezone } from '../../../../plugins/vis_type_timelion/public'; - -import 'uiExports/savedObjectTypes'; - -require('ui/i18n'); -require('ui/autoload/all'); - -// TODO: remove ui imports completely (move to plugins) -import 'ui/directives/input_focus'; -import './directives/saved_object_finder'; -import 'ui/directives/listen'; -import './directives/saved_object_save_as_checkbox'; -import './services/saved_sheet_register'; - -import rootTemplate from 'plugins/timelion/index.html'; - -import { loadKbnTopNavDirectives } from '../../../../plugins/kibana_legacy/public'; -loadKbnTopNavDirectives(npStart.plugins.navigation.ui); - -require('plugins/timelion/directives/cells/cells'); -require('plugins/timelion/directives/fixed_element'); -require('plugins/timelion/directives/fullscreen/fullscreen'); -require('plugins/timelion/directives/timelion_expression_input'); -require('plugins/timelion/directives/timelion_help/timelion_help'); -require('plugins/timelion/directives/timelion_interval/timelion_interval'); -require('plugins/timelion/directives/timelion_save_sheet'); -require('plugins/timelion/directives/timelion_load_sheet'); -require('plugins/timelion/directives/timelion_options_sheet'); - -document.title = 'Timelion - Kibana'; - -const app = require('ui/modules').get('apps/timelion', ['i18n', 'ngSanitize']); - -routes.enable(); - -routes.when('/:id?', { - template: rootTemplate, - reloadOnSearch: false, - k7Breadcrumbs: ($injector, $route) => - $injector.invoke($route.current.params.id ? getSavedSheetBreadcrumbs : getCreateBreadcrumbs), - badge: (uiCapabilities) => { - if (uiCapabilities.timelion.save) { - return undefined; - } - - return { - text: i18n.translate('timelion.badge.readOnly.text', { - defaultMessage: 'Read only', - }), - tooltip: i18n.translate('timelion.badge.readOnly.tooltip', { - defaultMessage: 'Unable to save Timelion sheets', - }), - iconType: 'glasses', - }; - }, - resolve: { - savedSheet: function (redirectWhenMissing, savedSheets, $route) { - return savedSheets - .get($route.current.params.id) - .then((savedSheet) => { - if ($route.current.params.id) { - npStart.core.chrome.recentlyAccessed.add( - savedSheet.getFullPath(), - savedSheet.title, - savedSheet.id - ); - } - return savedSheet; - }) - .catch( - redirectWhenMissing({ - search: '/', - }) - ); - }, - }, -}); - -const location = 'Timelion'; - -app.controller('timelion', function ( - $http, - $route, - $routeParams, - $scope, - $timeout, - AppState, - config, - kbnUrl -) { - // Keeping this at app scope allows us to keep the current page when the user - // switches to say, the timepicker. - $scope.page = config.get('timelion:showTutorial', true) ? 1 : 0; - $scope.setPage = (page) => ($scope.page = page); - - timefilter.enableAutoRefreshSelector(); - timefilter.enableTimeRangeSelector(); - - const savedVisualizations = npStart.plugins.visualizations.savedVisualizationsLoader; - const timezone = getTimezone(config); - - const defaultExpression = '.es(*)'; - const savedSheet = $route.current.locals.savedSheet; - - $scope.topNavMenu = getTopNavMenu(); - - $timeout(function () { - if (config.get('timelion:showTutorial', true)) { - $scope.toggleMenu('showHelp'); - } - }, 0); - - $scope.transient = {}; - $scope.state = new AppState(getStateDefaults()); - function getStateDefaults() { - return { - sheet: savedSheet.timelion_sheet, - selected: 0, - columns: savedSheet.timelion_columns, - rows: savedSheet.timelion_rows, - interval: savedSheet.timelion_interval, - }; - } - - function getTopNavMenu() { - const newSheetAction = { - id: 'new', - label: i18n.translate('timelion.topNavMenu.newSheetButtonLabel', { - defaultMessage: 'New', - }), - description: i18n.translate('timelion.topNavMenu.newSheetButtonAriaLabel', { - defaultMessage: 'New Sheet', - }), - run: function () { - kbnUrl.change('/'); - }, - testId: 'timelionNewButton', - }; - - const addSheetAction = { - id: 'add', - label: i18n.translate('timelion.topNavMenu.addChartButtonLabel', { - defaultMessage: 'Add', - }), - description: i18n.translate('timelion.topNavMenu.addChartButtonAriaLabel', { - defaultMessage: 'Add a chart', - }), - run: function () { - $scope.$evalAsync(() => $scope.newCell()); - }, - testId: 'timelionAddChartButton', - }; - - const saveSheetAction = { - id: 'save', - label: i18n.translate('timelion.topNavMenu.saveSheetButtonLabel', { - defaultMessage: 'Save', - }), - description: i18n.translate('timelion.topNavMenu.saveSheetButtonAriaLabel', { - defaultMessage: 'Save Sheet', - }), - run: () => { - $scope.$evalAsync(() => $scope.toggleMenu('showSave')); - }, - testId: 'timelionSaveButton', - }; - - const deleteSheetAction = { - id: 'delete', - label: i18n.translate('timelion.topNavMenu.deleteSheetButtonLabel', { - defaultMessage: 'Delete', - }), - description: i18n.translate('timelion.topNavMenu.deleteSheetButtonAriaLabel', { - defaultMessage: 'Delete current sheet', - }), - disableButton: function () { - return !savedSheet.id; - }, - run: function () { - const title = savedSheet.title; - function doDelete() { - savedSheet - .delete() - .then(() => { - toastNotifications.addSuccess( - i18n.translate('timelion.topNavMenu.delete.modal.successNotificationText', { - defaultMessage: `Deleted '{title}'`, - values: { title }, - }) - ); - kbnUrl.change('/'); - }) - .catch((error) => fatalError(error, location)); - } - - const confirmModalOptions = { - confirmButtonText: i18n.translate('timelion.topNavMenu.delete.modal.confirmButtonLabel', { - defaultMessage: 'Delete', - }), - title: i18n.translate('timelion.topNavMenu.delete.modalTitle', { - defaultMessage: `Delete Timelion sheet '{title}'?`, - values: { title }, - }), - }; - - $scope.$evalAsync(() => { - npStart.core.overlays - .openConfirm( - i18n.translate('timelion.topNavMenu.delete.modal.warningText', { - defaultMessage: `You can't recover deleted sheets.`, - }), - confirmModalOptions - ) - .then((isConfirmed) => { - if (isConfirmed) { - doDelete(); - } - }); - }); - }, - testId: 'timelionDeleteButton', - }; - - const openSheetAction = { - id: 'open', - label: i18n.translate('timelion.topNavMenu.openSheetButtonLabel', { - defaultMessage: 'Open', - }), - description: i18n.translate('timelion.topNavMenu.openSheetButtonAriaLabel', { - defaultMessage: 'Open Sheet', - }), - run: () => { - $scope.$evalAsync(() => $scope.toggleMenu('showLoad')); - }, - testId: 'timelionOpenButton', - }; - - const optionsAction = { - id: 'options', - label: i18n.translate('timelion.topNavMenu.optionsButtonLabel', { - defaultMessage: 'Options', - }), - description: i18n.translate('timelion.topNavMenu.optionsButtonAriaLabel', { - defaultMessage: 'Options', - }), - run: () => { - $scope.$evalAsync(() => $scope.toggleMenu('showOptions')); - }, - testId: 'timelionOptionsButton', - }; - - const helpAction = { - id: 'help', - label: i18n.translate('timelion.topNavMenu.helpButtonLabel', { - defaultMessage: 'Help', - }), - description: i18n.translate('timelion.topNavMenu.helpButtonAriaLabel', { - defaultMessage: 'Help', - }), - run: () => { - $scope.$evalAsync(() => $scope.toggleMenu('showHelp')); - }, - testId: 'timelionDocsButton', - }; - - if (capabilities.get().timelion.save) { - return [ - newSheetAction, - addSheetAction, - saveSheetAction, - deleteSheetAction, - openSheetAction, - optionsAction, - helpAction, - ]; - } - return [newSheetAction, addSheetAction, openSheetAction, optionsAction, helpAction]; - } - - let refresher; - const setRefreshData = function () { - if (refresher) $timeout.cancel(refresher); - const interval = timefilter.getRefreshInterval(); - if (interval.value > 0 && !interval.pause) { - function startRefresh() { - refresher = $timeout(function () { - if (!$scope.running) $scope.search(); - startRefresh(); - }, interval.value); - } - startRefresh(); - } - }; - - const init = function () { - $scope.running = false; - $scope.search(); - setRefreshData(); - - $scope.model = { - timeRange: timefilter.getTime(), - refreshInterval: timefilter.getRefreshInterval(), - }; - - $scope.$listen($scope.state, 'fetch_with_changes', $scope.search); - timefilter.getFetch$().subscribe($scope.search); - - $scope.opts = { - saveExpression: saveExpression, - saveSheet: saveSheet, - savedSheet: savedSheet, - state: $scope.state, - search: $scope.search, - dontShowHelp: function () { - config.set('timelion:showTutorial', false); - $scope.setPage(0); - $scope.closeMenus(); - }, - }; - - $scope.menus = { - showHelp: false, - showSave: false, - showLoad: false, - showOptions: false, - }; - - $scope.toggleMenu = (menuName) => { - const curState = $scope.menus[menuName]; - $scope.closeMenus(); - $scope.menus[menuName] = !curState; - }; - - $scope.closeMenus = () => { - _.forOwn($scope.menus, function (value, key) { - $scope.menus[key] = false; - }); - }; - }; - - $scope.onTimeUpdate = function ({ dateRange }) { - $scope.model.timeRange = { - ...dateRange, - }; - timefilter.setTime(dateRange); - }; - - $scope.onRefreshChange = function ({ isPaused, refreshInterval }) { - $scope.model.refreshInterval = { - pause: isPaused, - value: refreshInterval, - }; - timefilter.setRefreshInterval({ - pause: isPaused, - value: refreshInterval ? refreshInterval : $scope.refreshInterval.value, - }); - - setRefreshData(); - }; - - $scope.$watch( - function () { - return savedSheet.lastSavedTitle; - }, - function (newTitle) { - docTitle.change(savedSheet.id ? newTitle : undefined); - } - ); - - $scope.toggle = function (property) { - $scope[property] = !$scope[property]; - }; - - $scope.newSheet = function () { - kbnUrl.change('/', {}); - }; - - $scope.newCell = function () { - $scope.state.sheet.push(defaultExpression); - $scope.state.selected = $scope.state.sheet.length - 1; - $scope.safeSearch(); - }; - - $scope.setActiveCell = function (cell) { - $scope.state.selected = cell; - }; - - $scope.search = function () { - $scope.state.save(); - $scope.running = true; - - // parse the time range client side to make sure it behaves like other charts - const timeRangeBounds = timefilter.getBounds(); - - const httpResult = $http - .post('../api/timelion/run', { - sheet: $scope.state.sheet, - time: _.assignIn( - { - from: timeRangeBounds.min, - to: timeRangeBounds.max, - }, - { - interval: $scope.state.interval, - timezone: timezone, - } - ), - }) - .then((resp) => resp.data) - .catch((resp) => { - throw resp.data; - }); - - httpResult - .then(function (resp) { - $scope.stats = resp.stats; - $scope.sheet = resp.sheet; - _.each(resp.sheet, function (cell) { - if (cell.exception) { - $scope.state.selected = cell.plot; - } - }); - $scope.running = false; - }) - .catch(function (resp) { - $scope.sheet = []; - $scope.running = false; - - const err = new Error(resp.message); - err.stack = resp.stack; - toastNotifications.addError(err, { - title: i18n.translate('timelion.searchErrorTitle', { - defaultMessage: 'Timelion request error', - }), - }); - }); - }; - - $scope.safeSearch = _.debounce($scope.search, 500); - - function saveSheet() { - savedSheet.timelion_sheet = $scope.state.sheet; - savedSheet.timelion_interval = $scope.state.interval; - savedSheet.timelion_columns = $scope.state.columns; - savedSheet.timelion_rows = $scope.state.rows; - savedSheet.save().then(function (id) { - if (id) { - toastNotifications.addSuccess({ - title: i18n.translate('timelion.saveSheet.successNotificationText', { - defaultMessage: `Saved sheet '{title}'`, - values: { title: savedSheet.title }, - }), - 'data-test-subj': 'timelionSaveSuccessToast', - }); - - if (savedSheet.id !== $routeParams.id) { - kbnUrl.change('/{{id}}', { id: savedSheet.id }); - } - } - }); - } - - function saveExpression(title) { - savedVisualizations.get({ type: 'timelion' }).then(function (savedExpression) { - savedExpression.visState.params = { - expression: $scope.state.sheet[$scope.state.selected], - interval: $scope.state.interval, - }; - savedExpression.title = title; - savedExpression.visState.title = title; - savedExpression.save().then(function (id) { - if (id) { - toastNotifications.addSuccess( - i18n.translate('timelion.saveExpression.successNotificationText', { - defaultMessage: `Saved expression '{title}'`, - values: { title: savedExpression.title }, - }) - ); - } - }); - }); - } - - init(); -}); diff --git a/src/legacy/core_plugins/timelion/public/directives/cells/cells.js b/src/legacy/core_plugins/timelion/public/directives/cells/cells.js deleted file mode 100644 index 104af3b1043d6..0000000000000 --- a/src/legacy/core_plugins/timelion/public/directives/cells/cells.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import { move } from 'ui/utils/collection'; - -require('angular-sortable-view'); -require('plugins/timelion/directives/chart/chart'); -require('plugins/timelion/directives/timelion_grid'); - -const app = require('ui/modules').get('apps/timelion', ['angular-sortable-view']); -import html from './cells.html'; - -app.directive('timelionCells', function () { - return { - restrict: 'E', - scope: { - sheet: '=', - state: '=', - transient: '=', - onSearch: '=', - onSelect: '=', - }, - template: html, - link: function ($scope) { - $scope.removeCell = function (index) { - _.pullAt($scope.state.sheet, index); - $scope.onSearch(); - }; - - $scope.dropCell = function (item, partFrom, partTo, indexFrom, indexTo) { - $scope.onSelect(indexTo); - move($scope.sheet, indexFrom, indexTo); - }; - }, - }; -}); diff --git a/src/legacy/core_plugins/timelion/public/directives/fixed_element.js b/src/legacy/core_plugins/timelion/public/directives/fixed_element.js deleted file mode 100644 index e3a8b2184bb20..0000000000000 --- a/src/legacy/core_plugins/timelion/public/directives/fixed_element.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import $ from 'jquery'; - -const app = require('ui/modules').get('apps/timelion', []); -app.directive('fixedElementRoot', function () { - return { - restrict: 'A', - link: function ($elem) { - let fixedAt; - $(window).bind('scroll', function () { - const fixed = $('[fixed-element]', $elem); - const body = $('[fixed-element-body]', $elem); - const top = fixed.offset().top; - - if ($(window).scrollTop() > top) { - // This is a gross hack, but its better than it was. I guess - fixedAt = $(window).scrollTop(); - fixed.addClass(fixed.attr('fixed-element')); - body.addClass(fixed.attr('fixed-element-body')); - body.css({ top: fixed.height() }); - } - - if ($(window).scrollTop() < fixedAt) { - fixed.removeClass(fixed.attr('fixed-element')); - body.removeClass(fixed.attr('fixed-element-body')); - body.removeAttr('style'); - } - }); - }, - }; -}); diff --git a/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js b/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js deleted file mode 100644 index ae042310fd464..0000000000000 --- a/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js +++ /dev/null @@ -1,315 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import rison from 'rison-node'; -import { uiModules } from 'ui/modules'; -import 'ui/directives/input_focus'; -import savedObjectFinderTemplate from './saved_object_finder.html'; -import { savedSheetLoader } from '../services/saved_sheets'; -import { keyMap } from 'ui/directives/key_map'; -import { - PaginateControlsDirectiveProvider, - PaginateDirectiveProvider, -} from '../../../../../plugins/kibana_legacy/public'; -import { PER_PAGE_SETTING } from '../../../../../plugins/saved_objects/common'; -import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../../../plugins/visualizations/public'; - -const module = uiModules.get('kibana'); - -module - .directive('paginate', PaginateDirectiveProvider) - .directive('paginateControls', PaginateControlsDirectiveProvider) - .directive('savedObjectFinder', function ($location, kbnUrl, Private, config) { - return { - restrict: 'E', - scope: { - type: '@', - // optional make-url attr, sets the userMakeUrl in our scope - userMakeUrl: '=?makeUrl', - // optional on-choose attr, sets the userOnChoose in our scope - userOnChoose: '=?onChoose', - // optional useLocalManagement attr, removes link to management section - useLocalManagement: '=?useLocalManagement', - /** - * @type {function} - an optional function. If supplied an `Add new X` button is shown - * and this function is called when clicked. - */ - onAddNew: '=', - /** - * @{type} boolean - set this to true, if you don't want the search box above the - * table to automatically gain focus once loaded - */ - disableAutoFocus: '=', - }, - template: savedObjectFinderTemplate, - controllerAs: 'finder', - controller: function ($scope, $element) { - const self = this; - - // the text input element - const $input = $element.find('input[ng-model=filter]'); - - // The number of items to show in the list - $scope.perPage = config.get(PER_PAGE_SETTING); - - // the list that will hold the suggestions - const $list = $element.find('ul'); - - // the current filter string, used to check that returned results are still useful - let currentFilter = $scope.filter; - - // the most recently entered search/filter - let prevSearch; - - // the list of hits, used to render display - self.hits = []; - - self.service = savedSheetLoader; - self.properties = self.service.loaderProperties; - - filterResults(); - - /** - * Boolean that keeps track of whether hits are sorted ascending (true) - * or descending (false) by title - * @type {Boolean} - */ - self.isAscending = true; - - /** - * Sorts saved object finder hits either ascending or descending - * @param {Array} hits Array of saved finder object hits - * @return {Array} Array sorted either ascending or descending - */ - self.sortHits = function (hits) { - self.isAscending = !self.isAscending; - self.hits = self.isAscending - ? _.sortBy(hits, 'title') - : _.sortBy(hits, 'title').reverse(); - }; - - /** - * Passed the hit objects and will determine if the - * hit should have a url in the UI, returns it if so - * @return {string|null} - the url or nothing - */ - self.makeUrl = function (hit) { - if ($scope.userMakeUrl) { - return $scope.userMakeUrl(hit); - } - - if (!$scope.userOnChoose) { - return hit.url; - } - - return '#'; - }; - - self.preventClick = function ($event) { - $event.preventDefault(); - }; - - /** - * Called when a hit object is clicked, can override the - * url behavior if necessary. - */ - self.onChoose = function (hit, $event) { - if ($scope.userOnChoose) { - $scope.userOnChoose(hit, $event); - } - - const url = self.makeUrl(hit); - if (!url || url === '#' || url.charAt(0) !== '#') return; - - $event.preventDefault(); - - // we want the '/path', not '#/path' - kbnUrl.change(url.substr(1)); - }; - - $scope.$watch('filter', function (newFilter) { - // ensure that the currentFilter changes from undefined to '' - // which triggers - currentFilter = newFilter || ''; - filterResults(); - }); - - $scope.pageFirstItem = 0; - $scope.pageLastItem = 0; - $scope.onPageChanged = (page) => { - $scope.pageFirstItem = page.firstItem; - $scope.pageLastItem = page.lastItem; - }; - - //manages the state of the keyboard selector - self.selector = { - enabled: false, - index: -1, - }; - - self.getLabel = function () { - return _.words(self.properties.nouns).map(_.upperFirst).join(' '); - }; - - //key handler for the filter text box - self.filterKeyDown = function ($event) { - switch (keyMap[$event.keyCode]) { - case 'enter': - if (self.hitCount !== 1) return; - - const hit = self.hits[0]; - if (!hit) return; - - self.onChoose(hit, $event); - $event.preventDefault(); - break; - } - }; - - //key handler for the list items - self.hitKeyDown = function ($event, page, paginate) { - switch (keyMap[$event.keyCode]) { - case 'tab': - if (!self.selector.enabled) break; - - self.selector.index = -1; - self.selector.enabled = false; - - //if the user types shift-tab return to the textbox - //if the user types tab, set the focus to the currently selected hit. - if ($event.shiftKey) { - $input.focus(); - } else { - $list.find('li.active a').focus(); - } - - $event.preventDefault(); - break; - case 'down': - if (!self.selector.enabled) break; - - if (self.selector.index + 1 < page.length) { - self.selector.index += 1; - } - $event.preventDefault(); - break; - case 'up': - if (!self.selector.enabled) break; - - if (self.selector.index > 0) { - self.selector.index -= 1; - } - $event.preventDefault(); - break; - case 'right': - if (!self.selector.enabled) break; - - if (page.number < page.count) { - paginate.goToPage(page.number + 1); - self.selector.index = 0; - selectTopHit(); - } - $event.preventDefault(); - break; - case 'left': - if (!self.selector.enabled) break; - - if (page.number > 1) { - paginate.goToPage(page.number - 1); - self.selector.index = 0; - selectTopHit(); - } - $event.preventDefault(); - break; - case 'escape': - if (!self.selector.enabled) break; - - $input.focus(); - $event.preventDefault(); - break; - case 'enter': - if (!self.selector.enabled) break; - - const hitIndex = (page.number - 1) * paginate.perPage + self.selector.index; - const hit = self.hits[hitIndex]; - if (!hit) break; - - self.onChoose(hit, $event); - $event.preventDefault(); - break; - case 'shift': - break; - default: - $input.focus(); - break; - } - }; - - self.hitBlur = function () { - self.selector.index = -1; - self.selector.enabled = false; - }; - - self.manageObjects = function (type) { - $location.url('/management/kibana/objects?_a=' + rison.encode({ tab: type })); - }; - - self.hitCountNoun = function () { - return (self.hitCount === 1 ? self.properties.noun : self.properties.nouns).toLowerCase(); - }; - - function selectTopHit() { - setTimeout(function () { - //triggering a focus event kicks off a new angular digest cycle. - $list.find('a:first').focus(); - }, 0); - } - - function filterResults() { - if (!self.service) return; - if (!self.properties) return; - - // track the filter that we use for this search, - // but ensure that we don't search for the same - // thing twice. This is called from multiple places - // and needs to be smart about when it actually searches - const filter = currentFilter; - if (prevSearch === filter) return; - - prevSearch = filter; - - const isLabsEnabled = config.get(VISUALIZE_ENABLE_LABS_SETTING); - self.service.find(filter).then(function (hits) { - hits.hits = hits.hits.filter( - (hit) => isLabsEnabled || _.get(hit, 'type.stage') !== 'experimental' - ); - hits.total = hits.hits.length; - - // ensure that we don't display old results - // as we can't really cancel requests - if (currentFilter === filter) { - self.hitCount = hits.total; - self.hits = _.sortBy(hits.hits, 'title'); - } - }); - } - }, - }; - }); diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js b/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js deleted file mode 100644 index 8b4c28a50b732..0000000000000 --- a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js +++ /dev/null @@ -1,281 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * Timelion Expression Autocompleter - * - * This directive allows users to enter multiline timelion expressions. If the user has entered - * a valid expression and then types a ".", this directive will display a list of suggestions. - * - * Users can navigate suggestions using the arrow keys. When a user selects a suggestion, it's - * inserted into the expression and the caret position is updated to be inside of the newly- - * added function's parentheses. - * - * Beneath the hood, we use a PEG grammar to validate the Timelion expression and detect if - * the caret is in a position within the expression that allows functions to be suggested. - * - * NOTE: This directive doesn't work well with contenteditable divs. Challenges include: - * - You have to replace markup with newline characters and spaces when passing the expression - * to the grammar. - * - You have to do the opposite when loading a saved expression, so that it appears correctly - * within the contenteditable (i.e. replace newlines with
markup). - * - The Range and Selection APIs ignore newlines when providing caret position, so there is - * literally no way to insert suggestions into the correct place in a multiline expression - * that has more than a single consecutive newline. - */ - -import _ from 'lodash'; -import $ from 'jquery'; -import PEG from 'pegjs'; -import grammar from 'raw-loader!../../../../../plugins/vis_type_timelion/common/chain.peg'; -import timelionExpressionInputTemplate from './timelion_expression_input.html'; -import { - SUGGESTION_TYPE, - Suggestions, - suggest, - insertAtLocation, -} from './timelion_expression_input_helpers'; -import { comboBoxKeys } from '@elastic/eui'; -import { npStart } from 'ui/new_platform'; - -const Parser = PEG.generate(grammar); - -export function TimelionExpInput($http, $timeout) { - return { - restrict: 'E', - scope: { - rows: '=', - sheet: '=', - updateChart: '&', - shouldPopoverSuggestions: '@', - }, - replace: true, - template: timelionExpressionInputTemplate, - link: function (scope, elem) { - const argValueSuggestions = npStart.plugins.visTypeTimelion.getArgValueSuggestions(); - const expressionInput = elem.find('[data-expression-input]'); - const functionReference = {}; - let suggestibleFunctionLocation = {}; - - scope.suggestions = new Suggestions(); - - function init() { - $http.get('../api/timelion/functions').then(function (resp) { - Object.assign(functionReference, { - byName: _.keyBy(resp.data, 'name'), - list: resp.data, - }); - }); - } - - function setCaretOffset(caretOffset) { - // Wait for Angular to update the input with the new expression and *then* we can set - // the caret position. - $timeout(() => { - expressionInput.focus(); - expressionInput[0].selectionStart = expressionInput[0].selectionEnd = caretOffset; - scope.$apply(); - }, 0); - } - - function insertSuggestionIntoExpression(suggestionIndex) { - if (scope.suggestions.isEmpty()) { - return; - } - - const { min, max } = suggestibleFunctionLocation; - let insertedValue; - let insertPositionMinOffset = 0; - - switch (scope.suggestions.type) { - case SUGGESTION_TYPE.FUNCTIONS: { - // Position the caret inside of the function parentheses. - insertedValue = `${scope.suggestions.list[suggestionIndex].name}()`; - - // min advanced one to not replace function '.' - insertPositionMinOffset = 1; - break; - } - case SUGGESTION_TYPE.ARGUMENTS: { - // Position the caret after the '=' - insertedValue = `${scope.suggestions.list[suggestionIndex].name}=`; - break; - } - case SUGGESTION_TYPE.ARGUMENT_VALUE: { - // Position the caret after the argument value - insertedValue = `${scope.suggestions.list[suggestionIndex].name}`; - break; - } - } - - const updatedExpression = insertAtLocation( - insertedValue, - scope.sheet, - min + insertPositionMinOffset, - max - ); - scope.sheet = updatedExpression; - - const newCaretOffset = min + insertedValue.length; - setCaretOffset(newCaretOffset); - } - - function scrollToSuggestionAt(index) { - // We don't cache these because the list changes based on user input. - const suggestionsList = $('[data-suggestions-list]'); - const suggestionListItem = $('[data-suggestion-list-item]')[index]; - // Scroll to the position of the item relative to the list, not to the window. - suggestionsList.scrollTop(suggestionListItem.offsetTop - suggestionsList[0].offsetTop); - } - - function getCursorPosition() { - if (expressionInput.length) { - return expressionInput[0].selectionStart; - } - return null; - } - - async function getSuggestions() { - const suggestions = await suggest( - scope.sheet, - functionReference.list, - Parser, - getCursorPosition(), - argValueSuggestions - ); - - // We're using ES6 Promises, not $q, so we have to wrap this in $apply. - scope.$apply(() => { - if (suggestions) { - scope.suggestions.setList(suggestions.list, suggestions.type); - scope.suggestions.show(); - suggestibleFunctionLocation = suggestions.location; - $timeout(() => { - const suggestionsList = $('[data-suggestions-list]'); - suggestionsList.scrollTop(0); - }, 0); - return; - } - - suggestibleFunctionLocation = undefined; - scope.suggestions.reset(); - }); - } - - function isNavigationalKey(key) { - const keyCodes = _.values(comboBoxKeys); - return keyCodes.includes(key); - } - - scope.onFocusInput = () => { - // Wait for the caret position of the input to update and then we can get suggestions - // (which depends on the caret position). - $timeout(getSuggestions, 0); - }; - - scope.onBlurInput = () => { - scope.suggestions.hide(); - }; - - scope.onKeyDownInput = (e) => { - // If we've pressed any non-navigational keys, then the user has typed something and we - // can exit early without doing any navigation. The keyup handler will pull up suggestions. - if (!isNavigationalKey(e.key)) { - return; - } - - switch (e.keyCode) { - case comboBoxKeys.ARROW_UP: - if (scope.suggestions.isVisible) { - // Up and down keys navigate through suggestions. - e.preventDefault(); - scope.suggestions.stepForward(); - scrollToSuggestionAt(scope.suggestions.index); - } - break; - - case comboBoxKeys.ARROW_DOWN: - if (scope.suggestions.isVisible) { - // Up and down keys navigate through suggestions. - e.preventDefault(); - scope.suggestions.stepBackward(); - scrollToSuggestionAt(scope.suggestions.index); - } - break; - - case comboBoxKeys.TAB: - // If there are no suggestions or none is selected, the user tabs to the next input. - if (scope.suggestions.isEmpty() || scope.suggestions.index < 0) { - // Before letting the tab be handled to focus the next element - // we need to hide the suggestions, otherwise it will focus these - // instead of the time interval select. - scope.suggestions.hide(); - return; - } - - // If we have suggestions, complete the selected one. - e.preventDefault(); - insertSuggestionIntoExpression(scope.suggestions.index); - break; - - case comboBoxKeys.ENTER: - if (e.metaKey || e.ctrlKey) { - // Re-render the chart when the user hits CMD+ENTER. - e.preventDefault(); - scope.updateChart(); - } else if (!scope.suggestions.isEmpty()) { - // If the suggestions are open, complete the expression with the suggestion. - e.preventDefault(); - insertSuggestionIntoExpression(scope.suggestions.index); - } - break; - - case comboBoxKeys.ESCAPE: - e.preventDefault(); - scope.suggestions.hide(); - break; - } - }; - - scope.onKeyUpInput = (e) => { - // If the user isn't navigating, then we should update the suggestions based on their input. - if (!isNavigationalKey(e.key)) { - getSuggestions(); - } - }; - - scope.onClickExpression = () => { - getSuggestions(); - }; - - scope.onClickSuggestion = (index) => { - insertSuggestionIntoExpression(index); - }; - - scope.getActiveSuggestionId = () => { - if (scope.suggestions.isVisible && scope.suggestions.index > -1) { - return `timelionSuggestion${scope.suggestions.index}`; - } - return ''; - }; - - init(); - }, - }; -} diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_grid.js b/src/legacy/core_plugins/timelion/public/directives/timelion_grid.js deleted file mode 100644 index 256c35331d016..0000000000000 --- a/src/legacy/core_plugins/timelion/public/directives/timelion_grid.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import $ from 'jquery'; - -const app = require('ui/modules').get('apps/timelion', []); -app.directive('timelionGrid', function () { - return { - restrict: 'A', - scope: { - timelionGridRows: '=', - timelionGridColumns: '=', - }, - link: function ($scope, $elem) { - function init() { - setDimensions(); - } - - $scope.$on('$destroy', function () { - $(window).off('resize'); //remove the handler added earlier - }); - - $(window).resize(function () { - setDimensions(); - }); - - $scope.$watchMulti(['timelionGridColumns', 'timelionGridRows'], function () { - setDimensions(); - }); - - function setDimensions() { - const borderSize = 2; - const headerSize = 45 + 35 + 28 + 20 * 2; // chrome + subnav + buttons + (container padding) - const verticalPadding = 10; - - if ($scope.timelionGridColumns != null) { - $elem.width($elem.parent().width() / $scope.timelionGridColumns - borderSize * 2); - } - - if ($scope.timelionGridRows != null) { - $elem.height( - ($(window).height() - headerSize) / $scope.timelionGridRows - - (verticalPadding + borderSize * 2) - ); - } - } - - init(); - }, - }; -}); diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_help/timelion_help.js b/src/legacy/core_plugins/timelion/public/directives/timelion_help/timelion_help.js deleted file mode 100644 index 25f3df13153ba..0000000000000 --- a/src/legacy/core_plugins/timelion/public/directives/timelion_help/timelion_help.js +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import template from './timelion_help.html'; -import { i18n } from '@kbn/i18n'; -import { uiModules } from 'ui/modules'; -import _ from 'lodash'; -import moment from 'moment'; -import '../../components/timelionhelp_tabs_directive'; - -const app = uiModules.get('apps/timelion', []); - -app.directive('timelionHelp', function ($http) { - return { - restrict: 'E', - template, - controller: function ($scope) { - $scope.functions = { - list: [], - details: null, - }; - - $scope.activeTab = 'funcref'; - $scope.activateTab = function (tabName) { - $scope.activeTab = tabName; - }; - - function init() { - $scope.es = { - invalidCount: 0, - }; - - $scope.translations = { - nextButtonLabel: i18n.translate('timelion.help.nextPageButtonLabel', { - defaultMessage: 'Next', - }), - previousButtonLabel: i18n.translate('timelion.help.previousPageButtonLabel', { - defaultMessage: 'Previous', - }), - dontShowHelpButtonLabel: i18n.translate('timelion.help.dontShowHelpButtonLabel', { - defaultMessage: `Don't show this again`, - }), - strongNextText: i18n.translate('timelion.help.welcome.content.strongNextText', { - defaultMessage: 'Next', - }), - emphasizedEverythingText: i18n.translate( - 'timelion.help.welcome.content.emphasizedEverythingText', - { - defaultMessage: 'everything', - } - ), - notValidAdvancedSettingsPath: i18n.translate( - 'timelion.help.configuration.notValid.advancedSettingsPathText', - { - defaultMessage: 'Management / Kibana / Advanced Settings', - } - ), - validAdvancedSettingsPath: i18n.translate( - 'timelion.help.configuration.valid.advancedSettingsPathText', - { - defaultMessage: 'Management/Kibana/Advanced Settings', - } - ), - esAsteriskQueryDescription: i18n.translate( - 'timelion.help.querying.esAsteriskQueryDescriptionText', - { - defaultMessage: 'hey Elasticsearch, find everything in my default index', - } - ), - esIndexQueryDescription: i18n.translate( - 'timelion.help.querying.esIndexQueryDescriptionText', - { - defaultMessage: 'use * as the q (query) for the logstash-* index', - } - ), - strongAddText: i18n.translate('timelion.help.expressions.strongAddText', { - defaultMessage: 'Add', - }), - twoExpressionsDescriptionTitle: i18n.translate( - 'timelion.help.expressions.examples.twoExpressionsDescriptionTitle', - { - defaultMessage: 'Double the fun.', - } - ), - customStylingDescriptionTitle: i18n.translate( - 'timelion.help.expressions.examples.customStylingDescriptionTitle', - { - defaultMessage: 'Custom styling.', - } - ), - namedArgumentsDescriptionTitle: i18n.translate( - 'timelion.help.expressions.examples.namedArgumentsDescriptionTitle', - { - defaultMessage: 'Named arguments.', - } - ), - groupedExpressionsDescriptionTitle: i18n.translate( - 'timelion.help.expressions.examples.groupedExpressionsDescriptionTitle', - { - defaultMessage: 'Grouped expressions.', - } - ), - }; - - getFunctions(); - checkElasticsearch(); - } - - function getFunctions() { - return $http.get('../api/timelion/functions').then(function (resp) { - $scope.functions.list = resp.data; - }); - } - $scope.recheckElasticsearch = function () { - $scope.es.valid = null; - checkElasticsearch().then(function (valid) { - if (!valid) $scope.es.invalidCount++; - }); - }; - - function checkElasticsearch() { - return $http.get('../api/timelion/validate/es').then(function (resp) { - if (resp.data.ok) { - $scope.es.valid = true; - $scope.es.stats = { - min: moment(resp.data.min).format('LLL'), - max: moment(resp.data.max).format('LLL'), - field: resp.data.field, - }; - } else { - $scope.es.valid = false; - $scope.es.invalidReason = (function () { - try { - const esResp = JSON.parse(resp.data.resp.response); - return _.get(esResp, 'error.root_cause[0].reason'); - } catch (e) { - if (_.get(resp, 'data.resp.message')) return _.get(resp, 'data.resp.message'); - if (_.get(resp, 'data.resp.output.payload.message')) - return _.get(resp, 'data.resp.output.payload.message'); - return i18n.translate('timelion.help.unknownErrorMessage', { - defaultMessage: 'Unknown error', - }); - } - })(); - } - return $scope.es.valid; - }); - } - init(); - }, - }; -}); diff --git a/src/legacy/core_plugins/timelion/public/header.svg b/src/legacy/core_plugins/timelion/public/header.svg deleted file mode 100644 index 56f2f0dc51a6e..0000000000000 --- a/src/legacy/core_plugins/timelion/public/header.svg +++ /dev/null @@ -1,227 +0,0 @@ - - - - - - image/svg+xml - - Kibana-Full-Logo - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Kibana-Full-Logo - - - - - - - - - - - - - - - - - - - - - diff --git a/src/legacy/core_plugins/timelion/public/icon.svg b/src/legacy/core_plugins/timelion/public/icon.svg deleted file mode 100644 index ba9a704b3ade2..0000000000000 --- a/src/legacy/core_plugins/timelion/public/icon.svg +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - diff --git a/src/legacy/core_plugins/timelion/public/legacy.ts b/src/legacy/core_plugins/timelion/public/legacy.ts deleted file mode 100644 index 7980291e2d462..0000000000000 --- a/src/legacy/core_plugins/timelion/public/legacy.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from 'ui/new_platform'; -import { plugin } from '.'; -import { TimelionPluginSetupDependencies } from './plugin'; -import { LegacyDependenciesPlugin } from './shim'; - -const setupPlugins: Readonly = { - // Temporary solution - // It will be removed when all dependent services are migrated to the new platform. - __LEGACY: new LegacyDependenciesPlugin(), -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, setupPlugins); -export const start = pluginInstance.start(npStart.core, npStart.plugins); diff --git a/src/legacy/core_plugins/timelion/public/logo.png b/src/legacy/core_plugins/timelion/public/logo.png deleted file mode 100644 index 7a62253697a062d70fe8e8cccaad83638851af5d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14638 zcmX9_1z3~)*M3Gfn;?yJN(_c{J3>+#q-%h*AV^O|7$G6uA>Ab)NGJjV14N`-q@|># zzUTdayRMCG*TZ(s`JHp0`^2Ii>!^{FFpvNMK>kP_sSf}U0r0sJ5drw^ zfc4z^WxhP8Zzof5T~Qph`(2sQm|h{jNsA{OM=-fL0Y|V>DC+rLC(4H1uLkcP?8%Mp zQw}t{X*T?kblIHRpSoZBO?o5OXYaeuXut@ftfGRSP?7Z4kzYW-1sCyP#0BM-E#El1 zbzI_TYx9o46O;HIbcB>gD*$VF|NSu*U-=tOY8_gVra>)#~kDjbS$&P`@;C1&4K)D;>R0O(1;(1W&c2GVdl6*Vy zV89c9OdQ8O<((^6Y`n>&?S8#d{?rbD z7-6gw838O`QK{GMkNvXc=kwPy@I}9X@4i3fROo3;K?zt?FPOAAjXfdf-0D&gRsQP{GhO)$RASi6N0zUU9_Z|WwAxu}Q< zcAMrhZ=NTS3?Y^{Lr?WY{CrXK^2H8#I0VwKq_1w^!8gz=W`b68WqzPjx!Xh(kz7*D zb<7)iOvo>>^37(%+th{}fu+rkzB5kGYDRp3-!UXAs`YI^oP%d5x~b{>%a$|rRYGnJ zyPTy;Xw6+kB<4eNyVA!URAm_rCC*A@s52Hd07|Ry<6~Pe6o47 zVzQ&9202(SD1nf0BjXZ!1MYPjUp?+0@h|H%!lB{wOr+e|$#3i5BMLC`a!xoF_sDn| z@1yRM68t>(Wyr z=MHyh!5OyGv+I|n>CP-}Oi4?l=D45Y7l}U!?+N}~mdv3%C z7Ccr*S&@uQ5*Cf_kuVZPa`@>LBtW3782D2M0tSi;D2AlEWF2_LMr@guqs%UZ#B)_8Fx9}5 zk+vJAW`}=YCodRgd{qd2=2PVyeK(jIrfx=$Vsar-WVloDsJnL_=GB(ou@Wh#L%85XbI104Bj#b_3Bwbhraf2@ZEnxz<4OdhsB*z~ zQ$9x;fH(RXr;JR{(jmim!wlX13~S}D02N1!=wriliag<1?YXR9|E`KPZqgHZb7csV zG*MsNK5$pNHa|&Jn2o*~;}BUrHt5#3TWZdk{wFNq=Q&T7@hw>%Le{-cx#|-}7nvmu z_X(6Q(Pnu0Pf%NLMUx^W*$933c6v3gynFVekCQa)TFQzZ)`7R|itDo9dATrR5L_Bf$xIk4IAfE`Aja2j|7eme#UeB_d)!X%&MHdPD!t90k5l+lQc`@PHlL#8^Ds<+s&8)B#8VX`BO?T23Sz2; zbd5VLhDNR-ea&7qK(hWx=8Pz#{BT-{x-C%hkB}ceVq;56O1xZLwkS|H378usva7bd zJnDA#9?tq`p64T7-L6-d8oTu)X=!QhzSNzzMX3^P>=gxK>eJuB`(X;ktBH?8DOhQN z2E;HO)dxw>BZBtu6ete9y&79PD|nflNm`ms^CG|u-PY3Wb$V$71^J(m>djKrM0 z$d9ynkx`8(G=WRDin7i8F&~{o2uUU*#cE!dRis=NdweICAUOw5>_Z);dst&qEfgaB zk;=CvNY@l$n7ItY**$dagTEdpgua^*;T0rW8DcnDE#gX|uhHg%OxvCn3;%+0u0z>U z!-vv@S;{N6EhB7$85R9VBB^mqiEIjN=^`sDD_OQTH_zV;t!eNvsXl*xVubxf*1Z)n zxfh}b$%ZVGvH#OEGMX?4jFZ#S!V)yB%a@;h#L~p9sj_{|!MYKCf2e-Oo`xAwL+0Ol z8m8Bf;Ngt6Thx5jh*`zH!Y9>XE9AlklLJ2QD!74Oq#@Zd3gy8kC>Ww|G(l(;eAs*a z@IkI?ZAUXJHOYbwvIW8mytA^W0tjJJn9Y$NKYqML`dZuDukKG)n@c!Mwf=HA30Aiv z1EQ7F;qvV}ZmJlxjc9*kk!x5w4X$R9Y9m`H4vl!So<=-)IQ9-YeamyqmbKU1vM z#Kgps0ktCnvGn$5xREDG`i$d;ClV&c#zH#A^$iWro`SZ?%eXiLbXbtqcsBCrGti=W z*7bHOE3C7dawRcU#+6x^+)Zj@Tn8cal4ToJSURT=I_U}0=6soAxMR+_)#~xS@z7b_q zX>1lqYiweYc1KT;T{G!@L`Lmh+0E4-Jj4#o6oOEsa4Dgf`-MGb7=pl6Emc~NRm;ENtt`vXKF(-e7BA4-_wo<~ms$F8%> zzM>jD8?MoF$WAT(eVaVLekSIj+U(f(pK)=D4u0vm3-0qZk?q=S;jFRD6T5EOA|*^b zMcr*6Bt-*NxLy~JTZXYM|E_N&_S*M5V@LKYDl0u#F?K$1M5{wAi%}{I7@h zjkH0OA8amjd!|2hZ7p#(F=Ta2H?%{7D8yI4C;Ewr2aSFItzxf)Ou5E#sG8b{ z_;;9e8ScS{@pTkmu08wMK;8q&lWjjetP)}S>DRN5KL-Z~vobDhnc7`#9oI9#SG7g@ z7i($O##f=YKA6qrxVxm+p|?-chEjrEoiX5{T=T=^VqOa0>N}Dlx*A^`{>msf^pnn! ziZ1*_Y@?{`fu2v!%ezemQ`CXaTEl0uFF35CbG+X)cMM9$e=Aa|ikH1E=&Wdr6+>{x z4(#ezA#iuKqY4XHn3?5SzI@KjT6^;`gLiO;F}1{}YUOduy*MOO&yQDoN8E3^2##xg z^9;?*%)}F3ss)r57XBK4d5?O+Ob&PXH#OyjKpRK1H4$4;vOUIG2yMp4ba^+Xw;=+f z|JH1Y%!V1U&sIo-5@Idn!|=9TUan+CZLWd(8eGn)%-7qi-PNZxzD&PB0BS%PQjDg% z9QZ{jygd+HIq@%gXzd9IayWda5;oa78%(HBSh@RwI&cEW6E5RF{)VNBa<(Jsr=g2D z?3*yLmOBFF2LVQnFNf-cQJP=^j*GF ze1Ir@;z<1&4RnUyFu7B7)Q#PjPO+|sQf_|g%NILikx*YNO7jX_8UIz?(NAa*18&(*CJn@4$>7FZD)^S2?NF}7r~lkuc;a+K?-zk-0yC{EAk>>7^cr}fEyOD`@i!fsAmx)wGDygpiFxVX4XA;~XGxLkN5F}Xe; zEu21D)U?_%fFCLZ@iI|Z9gEt0WI4rR)$Eo*<0M-#@w9`k`&zMUr07V1CI4YW-51dwlzaOqfpT_-Cv&;lYx+P%KYn#XmZ9c-H;Qk_AY9SDGPF zcWu<>KxSs9Jc{jTec0gW=xBqOt#cF`r6ntUQ9izPrKzp0t$f?MnDF&#WOQaGF{M2@ zHV#c;8E@K1MGO$6zbtu84y9k1B{PDhOLEuhnx=X2;(^EyvYQrn9#Nl$!?jvrEtkl8 ziO+GsV(`BJF=ppY4+Xj)aB4z~q+r?2j43qr~&(7l{CB9-v0 zyG6z2o15Pu2US5D@U3tW*Vfyc%OmB2$IlfeFK`AO!>u%_ z)o~7HyvwjQW#Kb?9bG7k*PT%`tWsQDeCv6#J>hvK7#_;=1s@-3($5o zHRa`x1`KR=XFrgGFRifH>*0T^BwQ#V+;@GJPjV6yy_H05CpOR%l~b@WdO)0~+^=GM z>yzk&I9r<$14xpgTI^MTO=&an*5o&3cA4?$Z0tGJlL@ykDNCI&Co4agPS>$=@QIAX zXSb-Xv%(!|kjNY~H?dR4vP4|}+Q~WpVY0FMvliPkv2ji1saRebP9z3Phd_aglw?SEKuwfGbatjo$iy@zH2KFTqg~aQ&VYx)(1*C2oO4@9z1wp+1GAj z9dd%6=w;jFsN`+3L8sP6Ngm->LgtR5OPh)k2Yz zqwVqUL$7Qf?!T%kSkk*m(Z37~tUvummA)_Tz@5o(IsZMecei0iAin|!i@t{nKRMqI z!kN>-(5H@Ww39`pq_-vi{l{{(O!(TbpI2De({Y_KaW?tt@bBNh3S`Z~Kv`MY3!uCG zQN{e7%l^TX`?`<>=SWS_qrzg%G;Z=)YfH=Pw^S;o5CE~1Ty$Ols(O3lJUcox)K2~g z^+12{p1gB(p(NyZ!6OFBTIjkId^D`&Cu> zCQztsO(wt(lc|(#xaP_^3QG&6%ikA*H?Rv6T5&VHjRDls?Hj#hu_En(Y~RX_FsbPtU%< z+f~aXlzNtbqK-Ize0$%=g%0HTp5L!|Up+okI{W45pclxJM4$ruHX>?&H7zjhMD3e~ zaOAmbinZv+#tDZbc%0I?)P8 zKcFVbp*cEBdl$PkHSn*zs_N5&ySBYplY5l5_<%Add;@oRjn?YL%E9Af`z~4wK8ROSt$H%DBAe*ff(IEDs~whR1%3@bOa$ozi!QQ^zsm zCl!Ywq}XkKpC=J4Py(^J)4S<5bOW?<75sMoGL1>wb(l??fPjEuy|*kbZF&@NUsb4wPyohiS+@vv!&n!@si0IVM6IN#k2$r zXFf2Ge5%z}(^bJc_#P9c`){J)og&JgI^wsuezp6;{b|0qWH%I*U4yAtpCj$ce(jA& zx#JZUq5E)g^Jg$EmB@ey)y|X(jzv>*B%FIJC#sOQ5E1P%*%k6AJt-;a?bo(9%u#Ns z1P_14@fWOcBPeHHGr9}U==+gD>$8+OgVbfWlcwE5j*!&GdVO5{P~{g94IJ#V2}Li0 zn?^+*ZF9~oFv4A@YTSYE3Hg#M&*{mmJX_3`)60|M+B&vMlb8Qg3ccO=q=h%#K>LjmRzY+4`TjHD${=1l#J1hA$7w(?864x9hE{ZJVPVx<9u=w z6}>wV3T%Y}K;4sSdUdayDBisb6CAoB0gWASWKOS zuR`FzN9kZ(3^~2~gHCluVF)}L6|m^d&CLV0`T`fqU6iBaT1)x` z@o^X9i{36xQ~PQIC|+)w#S4h`I42wczms&57wvuAUcS58iv$)5In_9MLg6Q zQVZPl1)LjvT(8^lXj=T_&%HzbrUzJn-H3#m_Q!yCpn^S$Qr(x^n48&$=2pZ@a54RO*)ZA4Feq46DsUQSSOHW}0wnw{z-d^e$rmZm zXC^l5+_2$OwmQS&^W*sO)X;q^?(8}t@BGg()pPCcHARVmec4p_94IV#rHwwC=1_BgG5hgJ?xvu2a#OGci&IoL zUxI@! zBlA7ZT_!7t=)Mt*PEM4{*ylIbhO7Io##3q2|C(?Uk-1;gJg+{+*WsXT)Cjj1Z;2{d zUXTTevxIQBo1Wz+^*M0JRFO~5rx6SEG(%QDJ9 z$=EaN3tZn2uaDLW0Y|mCdsM)@$QXm_?_+8fJexmdTCqaghZHh4!w;+o1GxxHo84v# zzz$R`;wVe~Ui~vMm>_PQK<2%UyNO7mM4c#O5DBllAbA6~-e8MEzM`tiLIZjKvRpbN zBad2H7OW3H;CCw_V|8PoQI#qBHmayl+reZ$7M6vwB!64FzF*EiemV%WLZ^TS zM~=zXr9!i#GC6~TvBR0R zu;r^CKXJ1>F0pSk zvBNI>SDt2k49eNU9(Ae6^bFA2y8?59MgFGkV?8}@>o7J)J1B-TGo5gG2vbz8VPtyk z2MXRI@T6%oo#C*`vb|Y}$onqZ@N6*#NM?Pe)0og!H_KQ2Y1{eLL5_M=+4ya=It4D? zLum?$Udm>3!}Q4y$a#<*FPxMMlb^ZC2e!{%&-oWyNc`UKChXd8UZyunzBgrBI?Sl7 z={n`^Mv-|ebnRh58xP{2Q$_#`eT&JuRk0s<$JN62?quEc&B5w>Qs?Q@r;}>r z3Gq>tWC%|a>a|}cRsmcE_LsCUg6r0!qnEE2TWMA*_`#4p`@6MY+llY#`ZP_7TJql? z4^bt_Rr!3 zIx^Ym65-I&uHzq?){+w0H)XT=U2mOnQtE{2U3Fb8T;~(<>Ayv1;^SR~UL6i>EW7V1 z*ltg|BwbdIvrz3`JHCtI-BJkL)wN{ARfclE9o<8Gv;$-mJRicR`S8qS?B2!qO+I4S zF8uWAiUs?}W<4??Q}jhTTTxY&+g|0{RjlZtE)PcJnLqr15(3_QzBWvamI4cN5FN=5N&b% zLNL(H`m}QYzxoc}>k_Qu!a#>%Oyo@Ik*-)pf)*t}$+v(#ZxU2z7G^|g+k23%`%)Qm zxd)D~6pAt%{+Q|;7_8zSyzY&my`6=}76UYF-a!8gG0~&<;9w+eRN?k&`9YsGG}U$5 zK!mY{Ju}|sR!JCx))Q%WUa5O|9T`|bG?(?{VD^32=L{2zSdg%ObvK8u#9q*T@td@8 zn_E`u+3S!zzK{>6Dmu|Ox*PCb&F*)&ce?1@qNn=$WFN8pH&sdIEY#z42xtH7T-@f@LZ?8UUPX^8#XDF6sDvhh##y5^~D3@i~8`$jJ=Q#uu zp@m%ky2Qvh^f)mD04sY=M$#m`MxlG(ZagN!mxlgrM@m_3^4aV6uhyRfwM+g>AxTBa zCaSBi=O9|j{(d`9bPgcK9;i!n_;0~~se=kg6)^i@EkSU{g7#~A8~o@q=ZXG$ahlaE z&hmO~*!7M-9?r|5s7)MzhvS``%xQo$o((Bl7qcn+@#Dv^x7_*X3J=!^R#T#-wn^Z^ z`0NN64qTMK>GY55e)O&o(PM2AWj0*!WGN?aU@{*9vrkh<6D0UUoma6isdS%Eu%gHq zTRCCdq-32QgP|SyWzXB&937ZP)-jZ}*BhKVsZS?5chReJryCaIwa*kQ2ca!|{ z5L|?Cap6DRJ1b=4PUL?bX7N$!M#yTeU$$@)-k+;ey6#N0c}r9ShS2epby$dVM_|`- za$4E_sPny%uA^twyjb1-RCjI5w(7&xhD3=kuNH#l#^!%igMp|el=0@*xK3XgaX+_R za_t9l^IN0Hw=B1nkq?`D3vK>}!|I+;yUqUyw}G>NZDn5uyn`Ny`I(bmE@-&hjm2N*o z82Q_LBbJ7cs1*-=hmkpOK3%J*^!Z#+X=%pegggrNS6v+QDMM_V`@)lOE(o{gX&Fwz~&-~swVB!7PdI^2q6;KeZ!AIF(2vD3xe-}!BDeIg-H7eT*H z+DAzE@$_nJPzKw|6fQLU9PZq0XJjkle_Qp2)xj}S&oF@?se+YSJbwpf>%Dm-m(cc7 zFNueY|6_Pd@m)`aOVLcq2RIl3nHZ9B5$|B}Xp(VT{q_?}viy%yXy*37$bpp(k^!Go z&rQE`oxZwm22{E*>pn|zP^Va29%p$Gt|U3I)rJK5n!Gigiwk$U-JN<8rElNhO0e+M z^x%j{zx`g1QIzSA@q`o^_F@N`#Z=?}T1<=PN7BI}sukQxuR!J$M8ppqT=Y!WSKb9V ze?7g|7rcKTr?aK?&n&rKU6M;(9XQ?^ zC&i+UiZe@k7v$(Qz*{tVj~9RRUkgjig#@)-Ur8pXM|{uY`;WH9n!)~vKo-=ls>H>U zvUfV--AtpC?$q;;hW^z@bb5Zgym{|Oa96!?{CPOJS%v$f6FCrplG^*z3$NBwZA@~e zpJzjHGB}9(pELGD&iU_|KkVybwG$MliNYz zhw9Q@1cYTX`mp*1iuAgGhL$#7Btxt6f%eYvY?lbbyWio|rplP{_gl3$y2Ju8z53l) z2|#ikM^od%9563V?jJOL0fSvY$TVzbcKv8!yU*GeMv2(mFj%;b5P(JX1kGs{Y;5>YP61p#UF5)A@6-Y zIlDepHLsu;!~4Ba@_B=j4eI`Wf$I=E_bbQB*un z5Tc@e_^&_9%{9A|o(c_(j0lxgR?f{`wlI*4H^ zuJK2Rtl^pG&!4ydifuoVXZ|zcR01vh<|e*hUR|y2Kp+C?4s*?%TuWW^lH`u;S(((% z5 zF5vXfjQzucgeTV4OEQ_c=?JHhb1%tHORI-u#g#_1uo$mZ?l6Y{VC=iz2M=A{CWhA0|nU;8RGyo z+(!y66UED-cpu&PKwCyE==9GPI2rwl-9j2FB1hX6_E_?#aJRiA*AYkWF%H(b8pyNO zg}r`&sC}XHPB%Fa49vcP#()}&&+VR;0xuu}+=czc$^5Wl*GxaBM~?<1`PMWsc(hpj zgP@q+zq_-uvvms|w_NPTKM8@~8ho|$t={%|yvcs;A@pb&NRwzIwXy|Iu`el`{5$>5 zox>M<4Rc`l1>-E^pgO6$XOLn9u&ELpl-2LU7xI{9BeG>J;6rAK<&KwR=vQouT5TFz zcxz2f&7QSi`?VVqr5OomK@QL*E-74GT&w99f~ZqmI`U;taE4~8uP>^maY3j(uBsNs z+ZENPb%sMkAE5^xQ`dw?OBYc-exfts7GS8Nx9&b&XB#t&YEQwWa=bHk!$EsGXP&Co z?i@;^FMv~oPdT*kZHBed*SaQXH^?lL`d5i(c25aVp~;w361M-m`m)T#TnK1U8MSm; zuMLpRC8vm7g`iGdGn=ufMoVR+?wv7tfJfv=29yj>ad!9A8n+dLnN626&`Z8g*3(Me zJqm{>&^j3YdK080RK zclY${lD&U_#}#{JiKMZf89Ta+=wA(}Kl73NnJkk)sxq&t<4A#HQ@i+tclAM@jG39M()#;s>(=Soe~C@p2gjt} zu^Sr6u_%Jd@OcmRiyZl6mS__kkiixzE4JTcmfVF%Dk4pAS$p82gv%oF{RUb{gNzG} z_Ju{B=K_c&O{D+ROg|b}&S$JY02R;LB>*-=kCL6OU5k^uqNR@cQJwimY%wnT#O(i) zN?Z&CX@(vFs7U34S{bIe0ie^i-jv%pQ#=i`$5SZ(h?0q--x2VzS*Hhuz^#kUU^#ms z%f{Z8i0QO%oasa&brC)vN?dO70Cs5v6NPn&%M)VQbvPzhsYU*v5*r*-;xYvu`2F;Y zC+z%j&9~qLx!##k0jJJlys1%(JBhsetw_Jkd=nRCDPR0Y*RPydG;8U9Rg*^j6)`PBZIJ2ZWnj|mJHIwEt z7M;NYIVb^*_+{n55U>WXpWMP)y!=LLThnWoL9xuxz99d@qh%D=D{{c0!6j2X2eaq6 zUJ9^+mcqBGEpcfqJiD+oGV+Yw+S33wTr=0BxqA*kaPh{+$4|aw;UUZ>yo|9Glpt;ZAg!0-#nX8#4fVkPLrg8u^UBf>t} z_C$mLoUo7)Yyb!hR1mD=Iv7GLcwR8HJ2?1oA-17H)Yi@I`oj?}nW&g%qd_d=8|cw{ zcQFppAr8Q1-AV4=Xy0^1bW9Am+jEC+EfoB3vNy_UxYK=DTE6HXc=;T3WS!0}bF|NW zeP#axvTZp!ln{QM9?_vAcrCT^IV9Vzapo_Gonn9x`#&ng(3dY?Dn?c@o7clx68nT` zEEeko(cozG?-G8T1f+sKd85?>$Sbpg}0JR*E9m3LnjV*5TnVG$u6>aQ? z$Hu(h*u(cK8Ac7oR&N}>5K7YbsGr-7!FkzD2{;h3i3-qY;sV8;oi|ch!^VTyu1PUL zTd&S?gVHB}g&SR84^S6ywC;zJe~XbFa}7!BS>YvpqrAG{(PBB?^bq?g0VCU1GwLJB zq=TA-XbFR+0KX)c{EhMw_}&pHt2;KhBw19hWN67bbFy5lGaGXc(!DcTJ)UvFkF+QU z@+Wuw+QI$PNrX@EZMyx}*K<${JttY$h5XSwv6axb_4vqeaAlF=MKjaq$=zYOU;RAn zbU*5y!er)8BJ20jElv$4zohNv9R?mmoBSNG{^-{`${QwjyrDUrBG0isIvE5w(5bS) z-T>It2G`xT+>A?J!;Fir!o~(yp!JVmkh`(*q|xr~t_l*5Z*4w(yQ^--jT?X}AA9Sy zm#kJv)3}W+chP&8?iXmob;I2P#w8w+3wf7cm{_(@BQGoj+82iE}XLQ z&{}wEIW;h!b7C^Rd&&SHz?2IxLd~A8G>*!<3J3``XAQ42W?WFP2M&6)EWLMmGVkld zoy13phAR77(#H3X2jqqR@shk@EB3uJb}Pvghg(r$b%!D{CS9t1>iQ$4;x~^LVO7o# z3t7k_5ug2{s`?E}-tf)82fOpO{*p|avU!^y;LdOY-G^w>gffH`;laNjGu!yU(nz-v zK<&lpY?UQ}SEM)@ z0KV1MIkmyGKTEe`yulX8(ZHCBGv}7?JKv&85HDkLqmh#iANr$`OaAL$SF8@drV{tvRn7c z+S*#M>s;Y7`xZn~d%$74?S`v;A(RIV!q-hMh}yjG{<Vl6oA!oy z>d0UGR%ZW~QR1QtqJHgmn>=y%REch$Rl+8nik=^?dc07*%;=VHQK}v|Fm>OO^#|es zxclO#kFL7&s77r9FGtX|%kRZ+)Y!n!`3vJlLC_IsocYwf>t_NIoFnKMZHR=P)5*z6 zB+$ek{o}bO=!7ibeFmgvRw{bpfwH;gV%m&D*cC6yU^a9$aOxW;!bEXvL6m4_VG)d% zcxkob*X}tfw!48$#wxnR*wV$N9|eB*XyFNlUA3FlD~0*@64<|l_EDpa+56a8p+D<` zbj{${zbGYU-`U^MT6bz8y+TmaA^IM73yqaH1Qi`jLUxQgiLGC zhEfG1cl|CoxRbU5B6|+u=nQa;gYBzJM}-DpFpN5S4Qj@PYR1iPYHHH{Y;zzFguUhW zW-Lm+3xsA>E&a!7hf(HANL`QvKLx1LFCvk?^00v7oQaHpE?`9*~fjslDIbC1IU94DuG8}zs}=?O&_~>JkmV3ONlt@NBYtt6z=4>oH$z( z9cuEe^@D*+2-|(9f1{{z;n8$00{R*lD!H=3=LggP{)*Q*!pO)Sd5q~YR_NR#Cz+yvDohC$y-)*UZ5}lk`Lu>1>jA0Zh5!(%Sv8?d3%Vy4420as# zjm8-_N}Ly;SG=y65>5Y3K_nlHiHOXPVp&urp34(@(Za6znqSrIM1s7gwxi=Z4CI-e zn;}nO$`(Y+MyQ*p1rLez02Xvs`bEd)MGH>r^orNlOi^`|6Wky#WTSLSlIeb)Q>?>FOnqf*&ZSj2XsW$pL$rnG8LsMt7KUFqjmAGA=BD zQm9_df(OgYB{IqxA05d=5vihKhD7T0z>Ui)GJOZG_fZ#4)8w0uH6PC(u~u2Aef0-^ z1K-#3h>*udB4Rkly!8J#QWz*yPVM4Y*xR&2EVYzcD5lfL4vx68Ub?XM67HrSd2STlAgV`3m$9EH8e@9Zz2fS!k z%%&Urzf=$&vXY-!`$ke`mUIkbr_}kFF?i?g3%h;_%pybfV;N#Ti9K^ z1br*v@b3@{%(-W8-d5Wpw4+bKVO&>c9?-h$iV{u@dla(Mh>&yc=kAkmpRe# ztkI+8k^&$ic|GRhWGEfNpgnbY=cvgCV2X+g^LMn9O|SpSL3+uB)!8I z-9W0B(j66ENpn*ny*e;R{!IL(kc!x#i!cRFuu1ycM=6ZQx0VH5!!^&bba9h##sB;h zDW)^tw13s!FN%ftM;A9vkkb8CLZe^@;WJO?cmJJFfBnru#7B6*h)TD;*{=i9XRl@b o*KZ4#OrvYF^YDSA^IHhsSEhqJl#(j=+eYA#iVm_y+2+;%0W@q08~^|S diff --git a/src/legacy/core_plugins/timelion/public/plugin.ts b/src/legacy/core_plugins/timelion/public/plugin.ts deleted file mode 100644 index 1f837303a2b3d..0000000000000 --- a/src/legacy/core_plugins/timelion/public/plugin.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { - CoreSetup, - Plugin, - PluginInitializerContext, - IUiSettingsClient, - CoreStart, -} from 'kibana/public'; -import { getTimeChart } from './panels/timechart/timechart'; -import { Panel } from './panels/panel'; -import { LegacyDependenciesPlugin, LegacyDependenciesPluginSetup } from './shim'; -import { KibanaLegacyStart } from '../../../../plugins/kibana_legacy/public'; - -/** @internal */ -export interface TimelionVisualizationDependencies extends LegacyDependenciesPluginSetup { - uiSettings: IUiSettingsClient; - timelionPanels: Map; -} - -/** @internal */ -export interface TimelionPluginSetupDependencies { - // Temporary solution - __LEGACY: LegacyDependenciesPlugin; -} - -/** @internal */ -export class TimelionPlugin implements Plugin, void> { - initializerContext: PluginInitializerContext; - - constructor(initializerContext: PluginInitializerContext) { - this.initializerContext = initializerContext; - } - - public async setup(core: CoreSetup, { __LEGACY }: TimelionPluginSetupDependencies) { - const timelionPanels: Map = new Map(); - - const dependencies: TimelionVisualizationDependencies = { - uiSettings: core.uiSettings, - timelionPanels, - ...(await __LEGACY.setup(core, timelionPanels)), - }; - - this.registerPanels(dependencies); - } - - private registerPanels(dependencies: TimelionVisualizationDependencies) { - const timeChartPanel: Panel = getTimeChart(dependencies); - - dependencies.timelionPanels.set(timeChartPanel.name, timeChartPanel); - } - - public start(core: CoreStart, { kibanaLegacy }: { kibanaLegacy: KibanaLegacyStart }) { - kibanaLegacy.loadFontAwesome(); - } - - public stop(): void {} -} diff --git a/src/legacy/core_plugins/timelion/public/services/saved_sheets.ts b/src/legacy/core_plugins/timelion/public/services/saved_sheets.ts deleted file mode 100644 index 1fb29de83d3d7..0000000000000 --- a/src/legacy/core_plugins/timelion/public/services/saved_sheets.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { npStart } from 'ui/new_platform'; -// @ts-ignore -import { uiModules } from 'ui/modules'; -import { SavedObjectLoader } from '../../../../../plugins/saved_objects/public'; -import { createSavedSheetClass } from './_saved_sheet'; - -const module = uiModules.get('app/sheet'); - -const savedObjectsClient = npStart.core.savedObjects.client; -const services = { - savedObjectsClient, - indexPatterns: npStart.plugins.data.indexPatterns, - search: npStart.plugins.data.search, - chrome: npStart.core.chrome, - overlays: npStart.core.overlays, -}; - -const SavedSheet = createSavedSheetClass(services, npStart.core.uiSettings); - -export const savedSheetLoader = new SavedObjectLoader( - SavedSheet, - savedObjectsClient, - npStart.core.chrome -); -savedSheetLoader.urlFor = (id) => `#/${encodeURIComponent(id)}`; -// Customize loader properties since adding an 's' on type doesn't work for type 'timelion-sheet'. -savedSheetLoader.loaderProperties = { - name: 'timelion-sheet', - noun: 'Saved Sheets', - nouns: 'saved sheets', -}; - -// This is the only thing that gets injected into controllers -module.service('savedSheets', () => savedSheetLoader); diff --git a/src/legacy/core_plugins/timelion/public/shim/timelion_legacy_module.ts b/src/legacy/core_plugins/timelion/public/shim/timelion_legacy_module.ts deleted file mode 100644 index 8122259f1c991..0000000000000 --- a/src/legacy/core_plugins/timelion/public/shim/timelion_legacy_module.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import 'ngreact'; -import 'brace/mode/hjson'; -import 'brace/ext/searchbox'; -import 'ui/accessibility/kbn_ui_ace_keyboard_mode'; - -import { once } from 'lodash'; -// @ts-ignore -import { uiModules } from 'ui/modules'; -import { Panel } from '../panels/panel'; -// @ts-ignore -import { Chart } from '../directives/chart/chart'; -// @ts-ignore -import { TimelionInterval } from '../directives/timelion_interval/timelion_interval'; -// @ts-ignore -import { TimelionExpInput } from '../directives/timelion_expression_input'; -// @ts-ignore -import { TimelionExpressionSuggestions } from '../directives/timelion_expression_suggestions/timelion_expression_suggestions'; - -/** @internal */ -export const initTimelionLegacyModule = once((timelionPanels: Map): void => { - require('ui/state_management/app_state'); - - uiModules - .get('apps/timelion', []) - .controller('TimelionVisController', function ($scope: any) { - $scope.$on('timelionChartRendered', (event: any) => { - event.stopPropagation(); - $scope.renderComplete(); - }); - }) - .constant('timelionPanels', timelionPanels) - .directive('chart', Chart) - .directive('timelionInterval', TimelionInterval) - .directive('timelionExpressionSuggestions', TimelionExpressionSuggestions) - .directive('timelionExpressionInput', TimelionExpInput); -}); diff --git a/src/legacy/ui/public/state_management/__tests__/state.js b/src/legacy/ui/public/state_management/__tests__/state.js index cde123e6c1d85..b6c705e814509 100644 --- a/src/legacy/ui/public/state_management/__tests__/state.js +++ b/src/legacy/ui/public/state_management/__tests__/state.js @@ -21,6 +21,7 @@ import sinon from 'sinon'; import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import { encode as encodeRison } from 'rison-node'; +import uiRoutes from 'ui/routes'; import '../../private'; import { toastNotifications } from '../../notify'; import * as FatalErrorNS from '../../notify/fatal_error'; @@ -38,6 +39,8 @@ describe('State Management', () => { const sandbox = sinon.createSandbox(); afterEach(() => sandbox.restore()); + uiRoutes.enable(); + describe('Enabled', () => { let $rootScope; let $location; diff --git a/src/plugins/saved_objects/public/index.ts b/src/plugins/saved_objects/public/index.ts index 4f7a4ff7f196f..9140de316605c 100644 --- a/src/plugins/saved_objects/public/index.ts +++ b/src/plugins/saved_objects/public/index.ts @@ -36,6 +36,7 @@ export { isErrorNonFatal, } from './saved_object'; export { SavedObjectSaveOpts, SavedObjectKibanaServices, SavedObject } from './types'; +export { PER_PAGE_SETTING, LISTING_LIMIT_SETTING } from '../common'; export { SavedObjectsStart } from './plugin'; export const plugin = () => new SavedObjectsPublicPlugin(); diff --git a/src/plugins/timelion/kibana.json b/src/plugins/timelion/kibana.json index 55e492e8f23cd..d8c709d867a3f 100644 --- a/src/plugins/timelion/kibana.json +++ b/src/plugins/timelion/kibana.json @@ -1,8 +1,19 @@ { "id": "timelion", - "version": "0.0.1", - "kibanaVersion": "kibana", - "configPath": "timelion", - "ui": false, - "server": true + "version": "kibana", + "ui": true, + "server": true, + "requiredBundles": [ + "kibanaLegacy", + "kibanaUtils", + "savedObjects", + "visTypeTimelion" + ], + "requiredPlugins": [ + "visualizations", + "data", + "navigation", + "visTypeTimelion", + "kibanaLegacy" + ] } diff --git a/src/legacy/core_plugins/timelion/public/_app.scss b/src/plugins/timelion/public/_app.scss similarity index 100% rename from src/legacy/core_plugins/timelion/public/_app.scss rename to src/plugins/timelion/public/_app.scss diff --git a/src/plugins/timelion/public/app.js b/src/plugins/timelion/public/app.js new file mode 100644 index 0000000000000..0294e71084f98 --- /dev/null +++ b/src/plugins/timelion/public/app.js @@ -0,0 +1,661 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +import { i18n } from '@kbn/i18n'; + +import { createHashHistory } from 'history'; + +import { createKbnUrlStateStorage } from '../../kibana_utils/public'; +import { syncQueryStateWithUrl } from '../../data/public'; + +import { getSavedSheetBreadcrumbs, getCreateBreadcrumbs } from './breadcrumbs'; +import { + addFatalError, + registerListenEventListener, + watchMultiDecorator, +} from '../../kibana_legacy/public'; +import { getTimezone } from '../../vis_type_timelion/public'; +import { initCellsDirective } from './directives/cells/cells'; +import { initFullscreenDirective } from './directives/fullscreen/fullscreen'; +import { initFixedElementDirective } from './directives/fixed_element'; +import { initTimelionLoadSheetDirective } from './directives/timelion_load_sheet'; +import { initTimelionHelpDirective } from './directives/timelion_help/timelion_help'; +import { initTimelionSaveSheetDirective } from './directives/timelion_save_sheet'; +import { initTimelionOptionsSheetDirective } from './directives/timelion_options_sheet'; +import { initSavedObjectSaveAsCheckBoxDirective } from './directives/saved_object_save_as_checkbox'; +import { initSavedObjectFinderDirective } from './directives/saved_object_finder'; +import { initTimelionTabsDirective } from './components/timelionhelp_tabs_directive'; +import { initInputFocusDirective } from './directives/input_focus'; +import { Chart } from './directives/chart/chart'; +import { TimelionInterval } from './directives/timelion_interval/timelion_interval'; +import { timelionExpInput } from './directives/timelion_expression_input'; +import { TimelionExpressionSuggestions } from './directives/timelion_expression_suggestions/timelion_expression_suggestions'; +import { initSavedSheetService } from './services/saved_sheets'; +import { initTimelionAppState } from './timelion_app_state'; + +import rootTemplate from './index.html'; + +export function initTimelionApp(app, deps) { + app.run(registerListenEventListener); + + const savedSheetLoader = initSavedSheetService(app, deps); + + app.factory('history', () => createHashHistory()); + app.factory('kbnUrlStateStorage', (history) => + createKbnUrlStateStorage({ + history, + useHash: deps.core.uiSettings.get('state:storeInSessionStorage'), + }) + ); + app.config(watchMultiDecorator); + + app + .controller('TimelionVisController', function ($scope) { + $scope.$on('timelionChartRendered', (event) => { + event.stopPropagation(); + $scope.renderComplete(); + }); + }) + .constant('timelionPanels', deps.timelionPanels) + .directive('chart', Chart) + .directive('timelionInterval', TimelionInterval) + .directive('timelionExpressionSuggestions', TimelionExpressionSuggestions) + .directive('timelionExpressionInput', timelionExpInput(deps)); + + initTimelionHelpDirective(app); + initInputFocusDirective(app); + initTimelionTabsDirective(app, deps); + initSavedObjectFinderDirective(app, savedSheetLoader, deps.core.uiSettings); + initSavedObjectSaveAsCheckBoxDirective(app); + initCellsDirective(app); + initFixedElementDirective(app); + initFullscreenDirective(app); + initTimelionSaveSheetDirective(app); + initTimelionLoadSheetDirective(app); + initTimelionOptionsSheetDirective(app); + + const location = 'Timelion'; + + app.directive('timelionApp', function () { + return { + restrict: 'E', + controllerAs: 'timelionApp', + controller: timelionController, + }; + }); + + function timelionController( + $http, + $route, + $routeParams, + $scope, + $timeout, + history, + kbnUrlStateStorage + ) { + // Keeping this at app scope allows us to keep the current page when the user + // switches to say, the timepicker. + $scope.page = deps.core.uiSettings.get('timelion:showTutorial', true) ? 1 : 0; + $scope.setPage = (page) => ($scope.page = page); + const timefilter = deps.plugins.data.query.timefilter.timefilter; + + timefilter.enableAutoRefreshSelector(); + timefilter.enableTimeRangeSelector(); + + deps.core.chrome.docTitle.change('Timelion - Kibana'); + + // starts syncing `_g` portion of url with query services + const { stop: stopSyncingQueryServiceStateWithUrl } = syncQueryStateWithUrl( + deps.plugins.data.query, + kbnUrlStateStorage + ); + + const savedSheet = $route.current.locals.savedSheet; + + function getStateDefaults() { + return { + sheet: savedSheet.timelion_sheet, + selected: 0, + columns: savedSheet.timelion_columns, + rows: savedSheet.timelion_rows, + interval: savedSheet.timelion_interval, + }; + } + + const { stateContainer, stopStateSync } = initTimelionAppState({ + stateDefaults: getStateDefaults(), + kbnUrlStateStorage, + }); + + $scope.state = _.cloneDeep(stateContainer.getState()); + $scope.expression = _.clone($scope.state.sheet[$scope.state.selected]); + $scope.updatedSheets = []; + + const savedVisualizations = deps.plugins.visualizations.savedVisualizationsLoader; + const timezone = getTimezone(deps.core.uiSettings); + + const defaultExpression = '.es(*)'; + + $scope.topNavMenu = getTopNavMenu(); + + $timeout(function () { + if (deps.core.uiSettings.get('timelion:showTutorial', true)) { + $scope.toggleMenu('showHelp'); + } + }, 0); + + $scope.transient = {}; + + function getTopNavMenu() { + const newSheetAction = { + id: 'new', + label: i18n.translate('timelion.topNavMenu.newSheetButtonLabel', { + defaultMessage: 'New', + }), + description: i18n.translate('timelion.topNavMenu.newSheetButtonAriaLabel', { + defaultMessage: 'New Sheet', + }), + run: function () { + history.push('/'); + $route.reload(); + }, + testId: 'timelionNewButton', + }; + + const addSheetAction = { + id: 'add', + label: i18n.translate('timelion.topNavMenu.addChartButtonLabel', { + defaultMessage: 'Add', + }), + description: i18n.translate('timelion.topNavMenu.addChartButtonAriaLabel', { + defaultMessage: 'Add a chart', + }), + run: function () { + $scope.$evalAsync(() => $scope.newCell()); + }, + testId: 'timelionAddChartButton', + }; + + const saveSheetAction = { + id: 'save', + label: i18n.translate('timelion.topNavMenu.saveSheetButtonLabel', { + defaultMessage: 'Save', + }), + description: i18n.translate('timelion.topNavMenu.saveSheetButtonAriaLabel', { + defaultMessage: 'Save Sheet', + }), + run: () => { + $scope.$evalAsync(() => $scope.toggleMenu('showSave')); + }, + testId: 'timelionSaveButton', + }; + + const deleteSheetAction = { + id: 'delete', + label: i18n.translate('timelion.topNavMenu.deleteSheetButtonLabel', { + defaultMessage: 'Delete', + }), + description: i18n.translate('timelion.topNavMenu.deleteSheetButtonAriaLabel', { + defaultMessage: 'Delete current sheet', + }), + disableButton: function () { + return !savedSheet.id; + }, + run: function () { + const title = savedSheet.title; + function doDelete() { + savedSheet + .delete() + .then(() => { + deps.core.notifications.toasts.addSuccess( + i18n.translate('timelion.topNavMenu.delete.modal.successNotificationText', { + defaultMessage: `Deleted '{title}'`, + values: { title }, + }) + ); + history.push('/'); + }) + .catch((error) => addFatalError(deps.core.fatalErrors, error, location)); + } + + const confirmModalOptions = { + confirmButtonText: i18n.translate( + 'timelion.topNavMenu.delete.modal.confirmButtonLabel', + { + defaultMessage: 'Delete', + } + ), + title: i18n.translate('timelion.topNavMenu.delete.modalTitle', { + defaultMessage: `Delete Timelion sheet '{title}'?`, + values: { title }, + }), + }; + + $scope.$evalAsync(() => { + deps.core.overlays + .openConfirm( + i18n.translate('timelion.topNavMenu.delete.modal.warningText', { + defaultMessage: `You can't recover deleted sheets.`, + }), + confirmModalOptions + ) + .then((isConfirmed) => { + if (isConfirmed) { + doDelete(); + } + }); + }); + }, + testId: 'timelionDeleteButton', + }; + + const openSheetAction = { + id: 'open', + label: i18n.translate('timelion.topNavMenu.openSheetButtonLabel', { + defaultMessage: 'Open', + }), + description: i18n.translate('timelion.topNavMenu.openSheetButtonAriaLabel', { + defaultMessage: 'Open Sheet', + }), + run: () => { + $scope.$evalAsync(() => $scope.toggleMenu('showLoad')); + }, + testId: 'timelionOpenButton', + }; + + const optionsAction = { + id: 'options', + label: i18n.translate('timelion.topNavMenu.optionsButtonLabel', { + defaultMessage: 'Options', + }), + description: i18n.translate('timelion.topNavMenu.optionsButtonAriaLabel', { + defaultMessage: 'Options', + }), + run: () => { + $scope.$evalAsync(() => $scope.toggleMenu('showOptions')); + }, + testId: 'timelionOptionsButton', + }; + + const helpAction = { + id: 'help', + label: i18n.translate('timelion.topNavMenu.helpButtonLabel', { + defaultMessage: 'Help', + }), + description: i18n.translate('timelion.topNavMenu.helpButtonAriaLabel', { + defaultMessage: 'Help', + }), + run: () => { + $scope.$evalAsync(() => $scope.toggleMenu('showHelp')); + }, + testId: 'timelionDocsButton', + }; + + if (deps.core.application.capabilities.timelion.save) { + return [ + newSheetAction, + addSheetAction, + saveSheetAction, + deleteSheetAction, + openSheetAction, + optionsAction, + helpAction, + ]; + } + return [newSheetAction, addSheetAction, openSheetAction, optionsAction, helpAction]; + } + + let refresher; + const setRefreshData = function () { + if (refresher) $timeout.cancel(refresher); + const interval = timefilter.getRefreshInterval(); + if (interval.value > 0 && !interval.pause) { + function startRefresh() { + refresher = $timeout(function () { + if (!$scope.running) $scope.search(); + startRefresh(); + }, interval.value); + } + startRefresh(); + } + }; + + const init = function () { + $scope.running = false; + $scope.search(); + setRefreshData(); + + $scope.model = { + timeRange: timefilter.getTime(), + refreshInterval: timefilter.getRefreshInterval(), + }; + + const unsubscribeStateUpdates = stateContainer.subscribe((state) => { + const clonedState = _.cloneDeep(state); + $scope.updatedSheets.forEach((updatedSheet) => { + clonedState.sheet[updatedSheet.id] = updatedSheet.expression; + }); + $scope.state = clonedState; + $scope.opts.state = clonedState; + $scope.expression = _.clone($scope.state.sheet[$scope.state.selected]); + $scope.search(); + }); + + timefilter.getFetch$().subscribe($scope.search); + + $scope.opts = { + saveExpression: saveExpression, + saveSheet: saveSheet, + savedSheet: savedSheet, + state: _.cloneDeep(stateContainer.getState()), + search: $scope.search, + dontShowHelp: function () { + deps.core.uiSettings.set('timelion:showTutorial', false); + $scope.setPage(0); + $scope.closeMenus(); + }, + }; + + $scope.$watch('opts.state.rows', function (newRow) { + const state = stateContainer.getState(); + if (state.rows !== newRow) { + stateContainer.transitions.set('rows', newRow); + } + }); + + $scope.$watch('opts.state.columns', function (newColumn) { + const state = stateContainer.getState(); + if (state.columns !== newColumn) { + stateContainer.transitions.set('columns', newColumn); + } + }); + + $scope.menus = { + showHelp: false, + showSave: false, + showLoad: false, + showOptions: false, + }; + + $scope.toggleMenu = (menuName) => { + const curState = $scope.menus[menuName]; + $scope.closeMenus(); + $scope.menus[menuName] = !curState; + }; + + $scope.closeMenus = () => { + _.forOwn($scope.menus, function (value, key) { + $scope.menus[key] = false; + }); + }; + + $scope.$on('$destroy', () => { + stopSyncingQueryServiceStateWithUrl(); + unsubscribeStateUpdates(); + stopStateSync(); + }); + }; + + $scope.onTimeUpdate = function ({ dateRange }) { + $scope.model.timeRange = { + ...dateRange, + }; + timefilter.setTime(dateRange); + if (!$scope.running) $scope.search(); + }; + + $scope.onRefreshChange = function ({ isPaused, refreshInterval }) { + $scope.model.refreshInterval = { + pause: isPaused, + value: refreshInterval, + }; + timefilter.setRefreshInterval({ + pause: isPaused, + value: refreshInterval ? refreshInterval : $scope.refreshInterval.value, + }); + + setRefreshData(); + }; + + $scope.$watch( + function () { + return savedSheet.lastSavedTitle; + }, + function (newTitle) { + if (savedSheet.id && newTitle) { + deps.core.chrome.docTitle.change(newTitle); + } + } + ); + + $scope.$watch('expression', function (newExpression) { + const state = stateContainer.getState(); + if (state.sheet[state.selected] !== newExpression) { + const updatedSheet = $scope.updatedSheets.find( + (updatedSheet) => updatedSheet.id === state.selected + ); + if (updatedSheet) { + updatedSheet.expression = newExpression; + } else { + $scope.updatedSheets.push({ + id: state.selected, + expression: newExpression, + }); + } + } + }); + + $scope.toggle = function (property) { + $scope[property] = !$scope[property]; + }; + + $scope.changeInterval = function (interval) { + $scope.currentInterval = interval; + }; + + $scope.updateChart = function () { + const state = stateContainer.getState(); + const newSheet = _.clone(state.sheet); + if ($scope.updatedSheets.length) { + $scope.updatedSheets.forEach((updatedSheet) => { + newSheet[updatedSheet.id] = updatedSheet.expression; + }); + $scope.updatedSheets = []; + } + stateContainer.transitions.updateState({ + interval: $scope.currentInterval ? $scope.currentInterval : state.interval, + sheet: newSheet, + }); + }; + + $scope.newSheet = function () { + history.push('/'); + }; + + $scope.removeSheet = function (removedIndex) { + const state = stateContainer.getState(); + const newSheet = state.sheet.filter((el, index) => index !== removedIndex); + $scope.updatedSheets = $scope.updatedSheets.filter((el) => el.id !== removedIndex); + stateContainer.transitions.updateState({ + sheet: newSheet, + selected: removedIndex ? removedIndex - 1 : removedIndex, + }); + }; + + $scope.newCell = function () { + const state = stateContainer.getState(); + const newSheet = [...state.sheet, defaultExpression]; + stateContainer.transitions.updateState({ sheet: newSheet, selected: newSheet.length - 1 }); + }; + + $scope.setActiveCell = function (cell) { + const state = stateContainer.getState(); + if (state.selected !== cell) { + stateContainer.transitions.updateState({ sheet: $scope.state.sheet, selected: cell }); + } + }; + + $scope.search = function () { + $scope.running = true; + const state = stateContainer.getState(); + + // parse the time range client side to make sure it behaves like other charts + const timeRangeBounds = timefilter.getBounds(); + + const httpResult = $http + .post('../api/timelion/run', { + sheet: state.sheet, + time: _.assignIn( + { + from: timeRangeBounds.min, + to: timeRangeBounds.max, + }, + { + interval: state.interval, + timezone: timezone, + } + ), + }) + .then((resp) => resp.data) + .catch((resp) => { + throw resp.data; + }); + + httpResult + .then(function (resp) { + $scope.stats = resp.stats; + $scope.sheet = resp.sheet; + _.forEach(resp.sheet, function (cell) { + if (cell.exception && cell.plot !== state.selected) { + stateContainer.transitions.set('selected', cell.plot); + } + }); + $scope.running = false; + }) + .catch(function (resp) { + $scope.sheet = []; + $scope.running = false; + + const err = new Error(resp.message); + err.stack = resp.stack; + deps.core.notifications.toasts.addError(err, { + title: i18n.translate('timelion.searchErrorTitle', { + defaultMessage: 'Timelion request error', + }), + }); + }); + }; + + $scope.safeSearch = _.debounce($scope.search, 500); + + function saveSheet() { + const state = stateContainer.getState(); + savedSheet.timelion_sheet = state.sheet; + savedSheet.timelion_interval = state.interval; + savedSheet.timelion_columns = state.columns; + savedSheet.timelion_rows = state.rows; + savedSheet.save().then(function (id) { + if (id) { + deps.core.notifications.toasts.addSuccess({ + title: i18n.translate('timelion.saveSheet.successNotificationText', { + defaultMessage: `Saved sheet '{title}'`, + values: { title: savedSheet.title }, + }), + 'data-test-subj': 'timelionSaveSuccessToast', + }); + + if (savedSheet.id !== $routeParams.id) { + history.push(`/${savedSheet.id}`); + } + } + }); + } + + async function saveExpression(title) { + const vis = await deps.plugins.visualizations.createVis('timelion', { + title, + params: { + expression: $scope.state.sheet[$scope.state.selected], + interval: $scope.state.interval, + }, + }); + const state = deps.plugins.visualizations.convertFromSerializedVis(vis.serialize()); + const visSavedObject = await savedVisualizations.get(); + Object.assign(visSavedObject, state); + const id = await visSavedObject.save(); + if (id) { + deps.core.notifications.toasts.addSuccess( + i18n.translate('timelion.saveExpression.successNotificationText', { + defaultMessage: `Saved expression '{title}'`, + values: { title: state.title }, + }) + ); + } + } + + init(); + } + + app.config(function ($routeProvider) { + $routeProvider + .when('/:id?', { + template: rootTemplate, + reloadOnSearch: false, + k7Breadcrumbs: ($injector, $route) => + $injector.invoke( + $route.current.params.id ? getSavedSheetBreadcrumbs : getCreateBreadcrumbs + ), + badge: () => { + if (deps.core.application.capabilities.timelion.save) { + return undefined; + } + + return { + text: i18n.translate('timelion.badge.readOnly.text', { + defaultMessage: 'Read only', + }), + tooltip: i18n.translate('timelion.badge.readOnly.tooltip', { + defaultMessage: 'Unable to save Timelion sheets', + }), + iconType: 'glasses', + }; + }, + resolve: { + savedSheet: function (savedSheets, $route) { + return savedSheets + .get($route.current.params.id) + .then((savedSheet) => { + if ($route.current.params.id) { + deps.core.chrome.recentlyAccessed.add( + savedSheet.getFullPath(), + savedSheet.title, + savedSheet.id + ); + } + return savedSheet; + }) + .catch(); + }, + }, + }) + .otherwise('/'); + }); +} diff --git a/src/plugins/timelion/public/application.ts b/src/plugins/timelion/public/application.ts new file mode 100644 index 0000000000000..a398106d56f58 --- /dev/null +++ b/src/plugins/timelion/public/application.ts @@ -0,0 +1,153 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import './index.scss'; + +import { EuiIcon } from '@elastic/eui'; +import angular, { IModule } from 'angular'; +// required for `ngSanitize` angular module +import 'angular-sanitize'; +// required for ngRoute +import 'angular-route'; +import 'angular-sortable-view'; +import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; +import { + IUiSettingsClient, + CoreStart, + PluginInitializerContext, + AppMountParameters, +} from 'kibana/public'; +import { getTimeChart } from './panels/timechart/timechart'; +import { Panel } from './panels/panel'; + +import { + configureAppAngularModule, + createTopNavDirective, + createTopNavHelper, +} from '../../kibana_legacy/public'; +import { TimelionPluginDependencies } from './plugin'; +import { DataPublicPluginStart } from '../../data/public'; +// @ts-ignore +import { initTimelionApp } from './app'; + +export interface RenderDeps { + pluginInitializerContext: PluginInitializerContext; + mountParams: AppMountParameters; + core: CoreStart; + plugins: TimelionPluginDependencies; + timelionPanels: Map; +} + +export interface TimelionVisualizationDependencies { + uiSettings: IUiSettingsClient; + timelionPanels: Map; + data: DataPublicPluginStart; + $rootScope: any; + $compile: any; +} + +let angularModuleInstance: IModule | null = null; + +export const renderApp = (deps: RenderDeps) => { + if (!angularModuleInstance) { + angularModuleInstance = createLocalAngularModule(deps); + // global routing stuff + configureAppAngularModule( + angularModuleInstance, + { core: deps.core, env: deps.pluginInitializerContext.env }, + true + ); + initTimelionApp(angularModuleInstance, deps); + } + + const $injector = mountTimelionApp(deps.mountParams.appBasePath, deps.mountParams.element, deps); + + return () => { + $injector.get('$rootScope').$destroy(); + }; +}; + +function registerPanels(dependencies: TimelionVisualizationDependencies) { + const timeChartPanel: Panel = getTimeChart(dependencies); + + dependencies.timelionPanels.set(timeChartPanel.name, timeChartPanel); +} + +const mainTemplate = (basePath: string) => `
+ +
`; + +const moduleName = 'app/timelion'; + +const thirdPartyAngularDependencies = ['ngSanitize', 'ngRoute', 'react', 'angular-sortable-view']; + +function mountTimelionApp(appBasePath: string, element: HTMLElement, deps: RenderDeps) { + const mountpoint = document.createElement('div'); + mountpoint.setAttribute('class', 'timelionAppContainer'); + // eslint-disable-next-line + mountpoint.innerHTML = mainTemplate(appBasePath); + // bootstrap angular into detached element and attach it later to + // make angular-within-angular possible + const $injector = angular.bootstrap(mountpoint, [moduleName]); + + registerPanels({ + uiSettings: deps.core.uiSettings, + timelionPanels: deps.timelionPanels, + data: deps.plugins.data, + $rootScope: $injector.get('$rootScope'), + $compile: $injector.get('$compile'), + }); + element.appendChild(mountpoint); + return $injector; +} + +function createLocalAngularModule(deps: RenderDeps) { + createLocalI18nModule(); + createLocalIconModule(); + createLocalTopNavModule(deps.plugins.navigation); + + const dashboardAngularModule = angular.module(moduleName, [ + ...thirdPartyAngularDependencies, + 'app/timelion/TopNav', + 'app/timelion/I18n', + 'app/timelion/icon', + ]); + return dashboardAngularModule; +} + +function createLocalIconModule() { + angular + .module('app/timelion/icon', ['react']) + .directive('icon', (reactDirective) => reactDirective(EuiIcon)); +} + +function createLocalTopNavModule(navigation: TimelionPluginDependencies['navigation']) { + angular + .module('app/timelion/TopNav', ['react']) + .directive('kbnTopNav', createTopNavDirective) + .directive('kbnTopNavHelper', createTopNavHelper(navigation.ui)); +} + +function createLocalI18nModule() { + angular + .module('app/timelion/I18n', []) + .provider('i18n', I18nProvider) + .filter('i18n', i18nFilter) + .directive('i18nId', i18nDirective); +} diff --git a/src/legacy/core_plugins/timelion/public/breadcrumbs.js b/src/plugins/timelion/public/breadcrumbs.js similarity index 100% rename from src/legacy/core_plugins/timelion/public/breadcrumbs.js rename to src/plugins/timelion/public/breadcrumbs.js diff --git a/src/legacy/core_plugins/timelion/public/components/timelionhelp_tabs.js b/src/plugins/timelion/public/components/timelionhelp_tabs.js similarity index 95% rename from src/legacy/core_plugins/timelion/public/components/timelionhelp_tabs.js rename to src/plugins/timelion/public/components/timelionhelp_tabs.js index 639bd7d65a19e..7939afce412e1 100644 --- a/src/legacy/core_plugins/timelion/public/components/timelionhelp_tabs.js +++ b/src/plugins/timelion/public/components/timelionhelp_tabs.js @@ -54,6 +54,6 @@ export function TimelionHelpTabs(props) { } TimelionHelpTabs.propTypes = { - activeTab: PropTypes.string.isRequired, - activateTab: PropTypes.func.isRequired, + activeTab: PropTypes.string, + activateTab: PropTypes.func, }; diff --git a/src/legacy/core_plugins/timelion/public/directives/fullscreen/fullscreen.js b/src/plugins/timelion/public/components/timelionhelp_tabs_directive.js similarity index 56% rename from src/legacy/core_plugins/timelion/public/directives/fullscreen/fullscreen.js rename to src/plugins/timelion/public/components/timelionhelp_tabs_directive.js index 5c4bd72ceb708..67e0d595314f6 100644 --- a/src/legacy/core_plugins/timelion/public/directives/fullscreen/fullscreen.js +++ b/src/plugins/timelion/public/components/timelionhelp_tabs_directive.js @@ -17,23 +17,27 @@ * under the License. */ -require('angular-sortable-view'); -require('plugins/timelion/directives/chart/chart'); -require('plugins/timelion/directives/timelion_grid'); +import React from 'react'; +import { TimelionHelpTabs } from './timelionhelp_tabs'; -const app = require('ui/modules').get('apps/timelion', ['angular-sortable-view']); -import html from './fullscreen.html'; - -app.directive('timelionFullscreen', function () { - return { - restrict: 'E', - scope: { - expression: '=', - series: '=', - state: '=', - transient: '=', - onSearch: '=', - }, - template: html, - }; -}); +export function initTimelionTabsDirective(app, deps) { + app.directive('timelionHelpTabs', function (reactDirective) { + return reactDirective( + (props) => { + return ( + + + + ); + }, + [['activeTab'], ['activateTab', { watchDepth: 'reference' }]], + { + restrict: 'E', + scope: { + activeTab: '=', + activateTab: '=', + }, + } + ); + }); +} diff --git a/src/legacy/core_plugins/timelion/public/directives/_index.scss b/src/plugins/timelion/public/directives/_index.scss similarity index 100% rename from src/legacy/core_plugins/timelion/public/directives/_index.scss rename to src/plugins/timelion/public/directives/_index.scss diff --git a/src/legacy/core_plugins/timelion/public/directives/_timelion_expression_input.scss b/src/plugins/timelion/public/directives/_timelion_expression_input.scss similarity index 100% rename from src/legacy/core_plugins/timelion/public/directives/_timelion_expression_input.scss rename to src/plugins/timelion/public/directives/_timelion_expression_input.scss diff --git a/src/legacy/core_plugins/timelion/public/directives/cells/_cells.scss b/src/plugins/timelion/public/directives/cells/_cells.scss similarity index 100% rename from src/legacy/core_plugins/timelion/public/directives/cells/_cells.scss rename to src/plugins/timelion/public/directives/cells/_cells.scss diff --git a/src/legacy/core_plugins/timelion/public/directives/cells/_index.scss b/src/plugins/timelion/public/directives/cells/_index.scss similarity index 100% rename from src/legacy/core_plugins/timelion/public/directives/cells/_index.scss rename to src/plugins/timelion/public/directives/cells/_index.scss diff --git a/src/legacy/core_plugins/timelion/public/directives/cells/cells.html b/src/plugins/timelion/public/directives/cells/cells.html similarity index 100% rename from src/legacy/core_plugins/timelion/public/directives/cells/cells.html rename to src/plugins/timelion/public/directives/cells/cells.html diff --git a/src/legacy/core_plugins/timelion/public/shim/legacy_dependencies_plugin.ts b/src/plugins/timelion/public/directives/cells/cells.js similarity index 50% rename from src/legacy/core_plugins/timelion/public/shim/legacy_dependencies_plugin.ts rename to src/plugins/timelion/public/directives/cells/cells.js index f6c329d417f2b..36a1e80dd470e 100644 --- a/src/legacy/core_plugins/timelion/public/shim/legacy_dependencies_plugin.ts +++ b/src/plugins/timelion/public/directives/cells/cells.js @@ -17,31 +17,36 @@ * under the License. */ -import chrome from 'ui/chrome'; -import { CoreSetup, Plugin } from 'kibana/public'; -import { initTimelionLegacyModule } from './timelion_legacy_module'; -import { Panel } from '../panels/panel'; +import { move } from './collection'; +import { initTimelionGridDirective } from '../timelion_grid'; -/** @internal */ -export interface LegacyDependenciesPluginSetup { - $rootScope: any; - $compile: any; -} - -export class LegacyDependenciesPlugin - implements Plugin, void> { - public async setup(core: CoreSetup, timelionPanels: Map) { - initTimelionLegacyModule(timelionPanels); +import html from './cells.html'; - const $injector = await chrome.dangerouslyGetActiveInjector(); +export function initCellsDirective(app) { + initTimelionGridDirective(app); + app.directive('timelionCells', function () { return { - $rootScope: $injector.get('$rootScope'), - $compile: $injector.get('$compile'), - } as LegacyDependenciesPluginSetup; - } + restrict: 'E', + scope: { + sheet: '=', + state: '=', + transient: '=', + onSearch: '=', + onSelect: '=', + onRemoveSheet: '=', + }, + template: html, + link: function ($scope) { + $scope.removeCell = function (index) { + $scope.onRemoveSheet(index); + }; - public start() { - // nothing to do here yet - } + $scope.dropCell = function (item, partFrom, partTo, indexFrom, indexTo) { + move($scope.sheet, indexFrom, indexTo); + $scope.onSelect(indexTo); + }; + }, + }; + }); } diff --git a/src/plugins/timelion/public/directives/cells/collection.ts b/src/plugins/timelion/public/directives/cells/collection.ts new file mode 100644 index 0000000000000..b882a2bbe6e5b --- /dev/null +++ b/src/plugins/timelion/public/directives/cells/collection.ts @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +/** + * move an obj either up or down in the collection by + * injecting it either before/after the prev/next obj that + * satisfied the qualifier + * + * or, just from one index to another... + * + * @param {array} objs - the list to move the object within + * @param {number|any} obj - the object that should be moved, or the index that the object is currently at + * @param {number|boolean} below - the index to move the object to, or whether it should be moved up or down + * @param {function} qualifier - a lodash-y callback, object = _.where, string = _.pluck + * @return {array} - the objs argument + */ +export function move( + objs: any[], + obj: object | number, + below: number | boolean, + qualifier?: ((object: object, index: number) => any) | Record | string +): object[] { + const origI = _.isNumber(obj) ? obj : objs.indexOf(obj); + if (origI === -1) { + return objs; + } + + if (_.isNumber(below)) { + // move to a specific index + objs.splice(below, 0, objs.splice(origI, 1)[0]); + return objs; + } + + below = !!below; + qualifier = qualifier && _.iteratee(qualifier); + + const above = !below; + const finder = below ? _.findIndex : _.findLastIndex; + + // find the index of the next/previous obj that meets the qualifications + const targetI = finder(objs, (otherAgg, otherI) => { + if (below && otherI <= origI) { + return; + } + if (above && otherI >= origI) { + return; + } + return Boolean(_.isFunction(qualifier) && qualifier(otherAgg, otherI)); + }); + + if (targetI === -1) { + return objs; + } + + // place the obj at it's new index + objs.splice(targetI, 0, objs.splice(origI, 1)[0]); + return objs; +} diff --git a/src/legacy/core_plugins/timelion/public/directives/chart/chart.js b/src/plugins/timelion/public/directives/chart/chart.js similarity index 100% rename from src/legacy/core_plugins/timelion/public/directives/chart/chart.js rename to src/plugins/timelion/public/directives/chart/chart.js diff --git a/src/plugins/timelion/public/directives/fixed_element.js b/src/plugins/timelion/public/directives/fixed_element.js new file mode 100644 index 0000000000000..f57c391e7fcda --- /dev/null +++ b/src/plugins/timelion/public/directives/fixed_element.js @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import $ from 'jquery'; + +export function initFixedElementDirective(app) { + app.directive('fixedElementRoot', function () { + return { + restrict: 'A', + link: function ($elem) { + let fixedAt; + $(window).bind('scroll', function () { + const fixed = $('[fixed-element]', $elem); + const body = $('[fixed-element-body]', $elem); + const top = fixed.offset().top; + + if ($(window).scrollTop() > top) { + // This is a gross hack, but its better than it was. I guess + fixedAt = $(window).scrollTop(); + fixed.addClass(fixed.attr('fixed-element')); + body.addClass(fixed.attr('fixed-element-body')); + body.css({ top: fixed.height() }); + } + + if ($(window).scrollTop() < fixedAt) { + fixed.removeClass(fixed.attr('fixed-element')); + body.removeClass(fixed.attr('fixed-element-body')); + body.removeAttr('style'); + } + }); + }, + }; + }); +} diff --git a/src/legacy/core_plugins/timelion/public/directives/fullscreen/fullscreen.html b/src/plugins/timelion/public/directives/fullscreen/fullscreen.html similarity index 85% rename from src/legacy/core_plugins/timelion/public/directives/fullscreen/fullscreen.html rename to src/plugins/timelion/public/directives/fullscreen/fullscreen.html index 325c7eabb2b03..194596ba79d0e 100644 --- a/src/legacy/core_plugins/timelion/public/directives/fullscreen/fullscreen.html +++ b/src/plugins/timelion/public/directives/fullscreen/fullscreen.html @@ -1,5 +1,5 @@
-
+
diff --git a/src/plugins/timelion/public/directives/timelion_help/timelion_help.js b/src/plugins/timelion/public/directives/timelion_help/timelion_help.js new file mode 100644 index 0000000000000..27f29f3a740ba --- /dev/null +++ b/src/plugins/timelion/public/directives/timelion_help/timelion_help.js @@ -0,0 +1,166 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import template from './timelion_help.html'; +import { i18n } from '@kbn/i18n'; +import _ from 'lodash'; +import moment from 'moment'; + +export function initTimelionHelpDirective(app) { + app.directive('timelionHelp', function ($http) { + return { + restrict: 'E', + template, + controller: function ($scope) { + $scope.functions = { + list: [], + details: null, + }; + + $scope.activeTab = 'funcref'; + $scope.activateTab = function (tabName) { + $scope.activeTab = tabName; + }; + + function init() { + $scope.es = { + invalidCount: 0, + }; + + $scope.translations = { + nextButtonLabel: i18n.translate('timelion.help.nextPageButtonLabel', { + defaultMessage: 'Next', + }), + previousButtonLabel: i18n.translate('timelion.help.previousPageButtonLabel', { + defaultMessage: 'Previous', + }), + dontShowHelpButtonLabel: i18n.translate('timelion.help.dontShowHelpButtonLabel', { + defaultMessage: `Don't show this again`, + }), + strongNextText: i18n.translate('timelion.help.welcome.content.strongNextText', { + defaultMessage: 'Next', + }), + emphasizedEverythingText: i18n.translate( + 'timelion.help.welcome.content.emphasizedEverythingText', + { + defaultMessage: 'everything', + } + ), + notValidAdvancedSettingsPath: i18n.translate( + 'timelion.help.configuration.notValid.advancedSettingsPathText', + { + defaultMessage: 'Management / Kibana / Advanced Settings', + } + ), + validAdvancedSettingsPath: i18n.translate( + 'timelion.help.configuration.valid.advancedSettingsPathText', + { + defaultMessage: 'Management/Kibana/Advanced Settings', + } + ), + esAsteriskQueryDescription: i18n.translate( + 'timelion.help.querying.esAsteriskQueryDescriptionText', + { + defaultMessage: 'hey Elasticsearch, find everything in my default index', + } + ), + esIndexQueryDescription: i18n.translate( + 'timelion.help.querying.esIndexQueryDescriptionText', + { + defaultMessage: 'use * as the q (query) for the logstash-* index', + } + ), + strongAddText: i18n.translate('timelion.help.expressions.strongAddText', { + defaultMessage: 'Add', + }), + twoExpressionsDescriptionTitle: i18n.translate( + 'timelion.help.expressions.examples.twoExpressionsDescriptionTitle', + { + defaultMessage: 'Double the fun.', + } + ), + customStylingDescriptionTitle: i18n.translate( + 'timelion.help.expressions.examples.customStylingDescriptionTitle', + { + defaultMessage: 'Custom styling.', + } + ), + namedArgumentsDescriptionTitle: i18n.translate( + 'timelion.help.expressions.examples.namedArgumentsDescriptionTitle', + { + defaultMessage: 'Named arguments.', + } + ), + groupedExpressionsDescriptionTitle: i18n.translate( + 'timelion.help.expressions.examples.groupedExpressionsDescriptionTitle', + { + defaultMessage: 'Grouped expressions.', + } + ), + }; + + getFunctions(); + checkElasticsearch(); + } + + function getFunctions() { + return $http.get('../api/timelion/functions').then(function (resp) { + $scope.functions.list = resp.data; + }); + } + $scope.recheckElasticsearch = function () { + $scope.es.valid = null; + checkElasticsearch().then(function (valid) { + if (!valid) $scope.es.invalidCount++; + }); + }; + + function checkElasticsearch() { + return $http.get('../api/timelion/validate/es').then(function (resp) { + if (resp.data.ok) { + $scope.es.valid = true; + $scope.es.stats = { + min: moment(resp.data.min).format('LLL'), + max: moment(resp.data.max).format('LLL'), + field: resp.data.field, + }; + } else { + $scope.es.valid = false; + $scope.es.invalidReason = (function () { + try { + const esResp = JSON.parse(resp.data.resp.response); + return _.get(esResp, 'error.root_cause[0].reason'); + } catch (e) { + if (_.get(resp, 'data.resp.message')) return _.get(resp, 'data.resp.message'); + if (_.get(resp, 'data.resp.output.payload.message')) + return _.get(resp, 'data.resp.output.payload.message'); + return i18n.translate('timelion.help.unknownErrorMessage', { + defaultMessage: 'Unknown error', + }); + } + })(); + } + return $scope.es.valid; + }); + } + init(); + }, + }; + }); +} diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_interval/_index.scss b/src/plugins/timelion/public/directives/timelion_interval/_index.scss similarity index 100% rename from src/legacy/core_plugins/timelion/public/directives/timelion_interval/_index.scss rename to src/plugins/timelion/public/directives/timelion_interval/_index.scss diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_interval/_timelion_interval.scss b/src/plugins/timelion/public/directives/timelion_interval/_timelion_interval.scss similarity index 100% rename from src/legacy/core_plugins/timelion/public/directives/timelion_interval/_timelion_interval.scss rename to src/plugins/timelion/public/directives/timelion_interval/_timelion_interval.scss diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_interval/timelion_interval.html b/src/plugins/timelion/public/directives/timelion_interval/timelion_interval.html similarity index 100% rename from src/legacy/core_plugins/timelion/public/directives/timelion_interval/timelion_interval.html rename to src/plugins/timelion/public/directives/timelion_interval/timelion_interval.html diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_interval/timelion_interval.js b/src/plugins/timelion/public/directives/timelion_interval/timelion_interval.js similarity index 88% rename from src/legacy/core_plugins/timelion/public/directives/timelion_interval/timelion_interval.js rename to src/plugins/timelion/public/directives/timelion_interval/timelion_interval.js index 3750e15c000e7..84c285e617189 100644 --- a/src/legacy/core_plugins/timelion/public/directives/timelion_interval/timelion_interval.js +++ b/src/plugins/timelion/public/directives/timelion_interval/timelion_interval.js @@ -27,6 +27,7 @@ export function TimelionInterval($timeout) { scope: { // The interval model model: '=', + changeInterval: '=', }, template, link: function ($scope, $elem) { @@ -59,23 +60,23 @@ export function TimelionInterval($timeout) { }); $scope.$watch('interval', function (newVal, oldVal) { - if (newVal === oldVal) return; + if (newVal === oldVal || $scope.model === newVal) return; if (newVal === 'other') { $scope.otherInterval = oldVal; - $scope.model = $scope.otherInterval; + $scope.changeInterval($scope.otherInterval); $timeout(function () { $('input', $elem).select(); }, 0); } else { $scope.otherInterval = $scope.interval; - $scope.model = $scope.interval; + $scope.changeInterval($scope.interval); } }); $scope.$watch('otherInterval', function (newVal, oldVal) { - if (newVal === oldVal) return; - $scope.model = newVal; + if (newVal === oldVal || $scope.model === newVal) return; + $scope.changeInterval(newVal); }); }, }; diff --git a/src/legacy/core_plugins/timelion/public/shim/index.ts b/src/plugins/timelion/public/directives/timelion_load_sheet.js similarity index 76% rename from src/legacy/core_plugins/timelion/public/shim/index.ts rename to src/plugins/timelion/public/directives/timelion_load_sheet.js index cfc7b62ff4f86..e2cbdf4987c03 100644 --- a/src/legacy/core_plugins/timelion/public/shim/index.ts +++ b/src/plugins/timelion/public/directives/timelion_load_sheet.js @@ -17,4 +17,14 @@ * under the License. */ -export * from './legacy_dependencies_plugin'; +import template from '../partials/load_sheet.html'; + +export function initTimelionLoadSheetDirective(app) { + app.directive('timelionLoad', function () { + return { + replace: true, + restrict: 'E', + template, + }; + }); +} diff --git a/src/legacy/core_plugins/timelion/public/services/saved_sheet_register.ts b/src/plugins/timelion/public/directives/timelion_options_sheet.js similarity index 76% rename from src/legacy/core_plugins/timelion/public/services/saved_sheet_register.ts rename to src/plugins/timelion/public/directives/timelion_options_sheet.js index 7c8a2909238da..a86a47649b4fd 100644 --- a/src/legacy/core_plugins/timelion/public/services/saved_sheet_register.ts +++ b/src/plugins/timelion/public/directives/timelion_options_sheet.js @@ -16,4 +16,15 @@ * specific language governing permissions and limitations * under the License. */ -import './saved_sheets'; + +import template from '../partials/sheet_options.html'; + +export function initTimelionOptionsSheetDirective(app) { + app.directive('timelionOptions', function () { + return { + replace: true, + restrict: 'E', + template, + }; + }); +} diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_load_sheet.js b/src/plugins/timelion/public/directives/timelion_save_sheet.js similarity index 74% rename from src/legacy/core_plugins/timelion/public/directives/timelion_load_sheet.js rename to src/plugins/timelion/public/directives/timelion_save_sheet.js index d80770cbc2ae1..26b3a015fed95 100644 --- a/src/legacy/core_plugins/timelion/public/directives/timelion_load_sheet.js +++ b/src/plugins/timelion/public/directives/timelion_save_sheet.js @@ -17,14 +17,14 @@ * under the License. */ -import { uiModules } from 'ui/modules'; -import template from 'plugins/timelion/partials/load_sheet.html'; -const app = uiModules.get('apps/timelion', []); +import saveTemplate from '../partials/save_sheet.html'; -app.directive('timelionLoad', function () { - return { - replace: true, - restrict: 'E', - template, - }; -}); +export function initTimelionSaveSheetDirective(app) { + app.directive('timelionSave', function () { + return { + replace: true, + restrict: 'E', + template: saveTemplate, + }; + }); +} diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_options_sheet.js b/src/plugins/timelion/public/flot.js similarity index 72% rename from src/legacy/core_plugins/timelion/public/directives/timelion_options_sheet.js rename to src/plugins/timelion/public/flot.js index 067c831f09de5..1ccb40c93a3d6 100644 --- a/src/legacy/core_plugins/timelion/public/directives/timelion_options_sheet.js +++ b/src/plugins/timelion/public/flot.js @@ -17,14 +17,10 @@ * under the License. */ -import { uiModules } from 'ui/modules'; -import template from 'plugins/timelion/partials/sheet_options.html'; -const app = uiModules.get('apps/timelion', []); - -app.directive('timelionOptions', function () { - return { - replace: true, - restrict: 'E', - template, - }; -}); +import './webpackShims/jquery.flot'; +import './webpackShims/jquery.flot.time'; +import './webpackShims/jquery.flot.symbol'; +import './webpackShims/jquery.flot.crosshair'; +import './webpackShims/jquery.flot.selection'; +import './webpackShims/jquery.flot.stack'; +import './webpackShims/jquery.flot.axislabels'; diff --git a/src/legacy/core_plugins/timelion/public/index.html b/src/plugins/timelion/public/index.html similarity index 92% rename from src/legacy/core_plugins/timelion/public/index.html rename to src/plugins/timelion/public/index.html index b51bfb0be7f3c..54efae7f81ba7 100644 --- a/src/legacy/core_plugins/timelion/public/index.html +++ b/src/plugins/timelion/public/index.html @@ -1,4 +1,4 @@ -
+
@@ -55,6 +55,7 @@
diff --git a/src/legacy/core_plugins/timelion/public/index.scss b/src/plugins/timelion/public/index.scss similarity index 100% rename from src/legacy/core_plugins/timelion/public/index.scss rename to src/plugins/timelion/public/index.scss diff --git a/src/legacy/core_plugins/timelion/public/index.ts b/src/plugins/timelion/public/index.ts similarity index 100% rename from src/legacy/core_plugins/timelion/public/index.ts rename to src/plugins/timelion/public/index.ts diff --git a/src/legacy/core_plugins/timelion/public/lib/observe_resize.js b/src/plugins/timelion/public/lib/observe_resize.js similarity index 100% rename from src/legacy/core_plugins/timelion/public/lib/observe_resize.js rename to src/plugins/timelion/public/lib/observe_resize.js diff --git a/src/legacy/core_plugins/timelion/public/panels/panel.ts b/src/plugins/timelion/public/panels/panel.ts similarity index 100% rename from src/legacy/core_plugins/timelion/public/panels/panel.ts rename to src/plugins/timelion/public/panels/panel.ts diff --git a/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts b/src/plugins/timelion/public/panels/timechart/schema.ts similarity index 93% rename from src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts rename to src/plugins/timelion/public/panels/timechart/schema.ts index 087e166925327..b56d8a66110c2 100644 --- a/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts +++ b/src/plugins/timelion/public/panels/timechart/schema.ts @@ -17,31 +17,32 @@ * under the License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import '../../../../../../plugins/vis_type_timelion/public/flot'; +import '../../flot'; import _ from 'lodash'; import $ from 'jquery'; import moment from 'moment-timezone'; -import { timefilter } from 'ui/timefilter'; // @ts-ignore import observeResize from '../../lib/observe_resize'; import { calculateInterval, DEFAULT_TIME_FORMAT, - // @ts-ignore -} from '../../../../../../plugins/vis_type_timelion/common/lib'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { tickFormatters } from '../../../../../../plugins/vis_type_timelion/public/helpers/tick_formatters'; -import { TimelionVisualizationDependencies } from '../../plugin'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { xaxisFormatterProvider } from '../../../../../../plugins/vis_type_timelion/public/helpers/xaxis_formatter'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { generateTicksProvider } from '../../../../../../plugins/vis_type_timelion/public/helpers/tick_generator'; + tickFormatters, + xaxisFormatterProvider, + generateTicksProvider, +} from '../../../../vis_type_timelion/public'; +import { TimelionVisualizationDependencies } from '../../application'; const DEBOUNCE_DELAY = 50; export function timechartFn(dependencies: TimelionVisualizationDependencies) { - const { $rootScope, $compile, uiSettings } = dependencies; + const { + $rootScope, + $compile, + uiSettings, + data: { + query: { timefilter }, + }, + } = dependencies; return function () { return { @@ -199,7 +200,7 @@ export function timechartFn(dependencies: TimelionVisualizationDependencies) { }); $elem.on('plotselected', function (event: any, ranges: any) { - timefilter.setTime({ + timefilter.timefilter.setTime({ from: moment(ranges.xaxis.from), to: moment(ranges.xaxis.to), }); @@ -299,7 +300,7 @@ export function timechartFn(dependencies: TimelionVisualizationDependencies) { const options = _.cloneDeep(defaultOptions) as any; // Get the X-axis tick format - const time = timefilter.getBounds() as any; + const time = timefilter.timefilter.getBounds() as any; const interval = calculateInterval( time.min.valueOf(), time.max.valueOf(), diff --git a/src/legacy/core_plugins/timelion/public/panels/timechart/timechart.ts b/src/plugins/timelion/public/panels/timechart/timechart.ts similarity index 94% rename from src/legacy/core_plugins/timelion/public/panels/timechart/timechart.ts rename to src/plugins/timelion/public/panels/timechart/timechart.ts index 4173bfeb331e2..525a994e3121d 100644 --- a/src/legacy/core_plugins/timelion/public/panels/timechart/timechart.ts +++ b/src/plugins/timelion/public/panels/timechart/timechart.ts @@ -19,7 +19,7 @@ import { timechartFn } from './schema'; import { Panel } from '../panel'; -import { TimelionVisualizationDependencies } from '../../plugin'; +import { TimelionVisualizationDependencies } from '../../application'; export function getTimeChart(dependencies: TimelionVisualizationDependencies) { // Schema is broken out so that it may be extended for use in other plugins diff --git a/src/legacy/core_plugins/timelion/public/partials/load_sheet.html b/src/plugins/timelion/public/partials/load_sheet.html similarity index 100% rename from src/legacy/core_plugins/timelion/public/partials/load_sheet.html rename to src/plugins/timelion/public/partials/load_sheet.html diff --git a/src/legacy/core_plugins/timelion/public/partials/save_sheet.html b/src/plugins/timelion/public/partials/save_sheet.html similarity index 100% rename from src/legacy/core_plugins/timelion/public/partials/save_sheet.html rename to src/plugins/timelion/public/partials/save_sheet.html diff --git a/src/legacy/core_plugins/timelion/public/partials/sheet_options.html b/src/plugins/timelion/public/partials/sheet_options.html similarity index 100% rename from src/legacy/core_plugins/timelion/public/partials/sheet_options.html rename to src/plugins/timelion/public/partials/sheet_options.html diff --git a/src/plugins/timelion/public/plugin.ts b/src/plugins/timelion/public/plugin.ts new file mode 100644 index 0000000000000..a92ced20cb6d1 --- /dev/null +++ b/src/plugins/timelion/public/plugin.ts @@ -0,0 +1,134 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BehaviorSubject } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { + CoreSetup, + CoreStart, + Plugin, + PluginInitializerContext, + DEFAULT_APP_CATEGORIES, + AppMountParameters, + AppUpdater, + ScopedHistory, +} from '../../../core/public'; +import { Panel } from './panels/panel'; +import { initAngularBootstrap, KibanaLegacyStart } from '../../kibana_legacy/public'; +import { createKbnUrlTracker } from '../../kibana_utils/public'; +import { DataPublicPluginStart, esFilters, DataPublicPluginSetup } from '../../data/public'; +import { NavigationPublicPluginStart } from '../../navigation/public'; +import { VisualizationsStart } from '../../visualizations/public'; +import { VisTypeTimelionPluginStart } from '../../vis_type_timelion/public'; + +export interface TimelionPluginDependencies { + data: DataPublicPluginStart; + navigation: NavigationPublicPluginStart; + visualizations: VisualizationsStart; + visTypeTimelion: VisTypeTimelionPluginStart; +} + +/** @internal */ +export class TimelionPlugin implements Plugin { + initializerContext: PluginInitializerContext; + private appStateUpdater = new BehaviorSubject(() => ({})); + private stopUrlTracking: (() => void) | undefined = undefined; + private currentHistory: ScopedHistory | undefined = undefined; + + constructor(initializerContext: PluginInitializerContext) { + this.initializerContext = initializerContext; + } + + public setup(core: CoreSetup, { data }: { data: DataPublicPluginSetup }) { + const timelionPanels: Map = new Map(); + + const { appMounted, appUnMounted, stop: stopUrlTracker } = createKbnUrlTracker({ + baseUrl: core.http.basePath.prepend('/app/timelion'), + defaultSubUrl: '#/', + storageKey: `lastUrl:${core.http.basePath.get()}:timelion`, + navLinkUpdater$: this.appStateUpdater, + toastNotifications: core.notifications.toasts, + stateParams: [ + { + kbnUrlKey: '_g', + stateUpdate$: data.query.state$.pipe( + filter( + ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval) + ), + map(({ state }) => ({ + ...state, + filters: state.filters?.filter(esFilters.isFilterPinned), + })) + ), + }, + ], + getHistory: () => this.currentHistory!, + }); + + this.stopUrlTracking = () => { + stopUrlTracker(); + }; + + initAngularBootstrap(); + core.application.register({ + id: 'timelion', + title: 'Timelion', + order: 8000, + defaultPath: '#/', + euiIconType: 'timelionApp', + category: DEFAULT_APP_CATEGORIES.kibana, + updater$: this.appStateUpdater.asObservable(), + mount: async (params: AppMountParameters) => { + const [coreStart, pluginsStart] = await core.getStartServices(); + this.currentHistory = params.history; + + appMounted(); + + const unlistenParentHistory = params.history.listen(() => { + window.dispatchEvent(new HashChangeEvent('hashchange')); + }); + + const { renderApp } = await import('./application'); + params.element.classList.add('timelionAppContainer'); + const unmount = renderApp({ + mountParams: params, + pluginInitializerContext: this.initializerContext, + timelionPanels, + core: coreStart, + plugins: pluginsStart as TimelionPluginDependencies, + }); + return () => { + unlistenParentHistory(); + unmount(); + appUnMounted(); + }; + }, + }); + } + + public start(core: CoreStart, { kibanaLegacy }: { kibanaLegacy: KibanaLegacyStart }) { + kibanaLegacy.loadFontAwesome(); + } + + public stop(): void { + if (this.stopUrlTracking) { + this.stopUrlTracking(); + } + } +} diff --git a/src/legacy/core_plugins/timelion/public/services/_saved_sheet.ts b/src/plugins/timelion/public/services/_saved_sheet.ts similarity index 95% rename from src/legacy/core_plugins/timelion/public/services/_saved_sheet.ts rename to src/plugins/timelion/public/services/_saved_sheet.ts index 4e5aa8d445e7d..0958cce860126 100644 --- a/src/legacy/core_plugins/timelion/public/services/_saved_sheet.ts +++ b/src/plugins/timelion/public/services/_saved_sheet.ts @@ -18,10 +18,7 @@ */ import { IUiSettingsClient } from 'kibana/public'; -import { - createSavedObjectClass, - SavedObjectKibanaServices, -} from '../../../../../plugins/saved_objects/public'; +import { createSavedObjectClass, SavedObjectKibanaServices } from '../../../saved_objects/public'; // Used only by the savedSheets service, usually no reason to change this export function createSavedSheetClass( diff --git a/src/plugins/timelion/public/services/saved_sheets.ts b/src/plugins/timelion/public/services/saved_sheets.ts new file mode 100644 index 0000000000000..a3e7f66d9ee47 --- /dev/null +++ b/src/plugins/timelion/public/services/saved_sheets.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectLoader } from '../../../saved_objects/public'; +import { createSavedSheetClass } from './_saved_sheet'; +import { RenderDeps } from '../application'; + +export function initSavedSheetService(app: angular.IModule, deps: RenderDeps) { + const savedObjectsClient = deps.core.savedObjects.client; + const services = { + savedObjectsClient, + indexPatterns: deps.plugins.data.indexPatterns, + search: deps.plugins.data.search, + chrome: deps.core.chrome, + overlays: deps.core.overlays, + }; + + const SavedSheet = createSavedSheetClass(services, deps.core.uiSettings); + + const savedSheetLoader = new SavedObjectLoader(SavedSheet, savedObjectsClient, deps.core.chrome); + savedSheetLoader.urlFor = (id) => `#/${encodeURIComponent(id)}`; + // Customize loader properties since adding an 's' on type doesn't work for type 'timelion-sheet'. + savedSheetLoader.loaderProperties = { + name: 'timelion-sheet', + noun: 'Saved Sheets', + nouns: 'saved sheets', + }; + // This is the only thing that gets injected into controllers + app.service('savedSheets', function () { + return savedSheetLoader; + }); + + return savedSheetLoader; +} diff --git a/src/plugins/timelion/public/timelion_app_state.ts b/src/plugins/timelion/public/timelion_app_state.ts new file mode 100644 index 0000000000000..43382adbf8f80 --- /dev/null +++ b/src/plugins/timelion/public/timelion_app_state.ts @@ -0,0 +1,73 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createStateContainer, syncState, IKbnUrlStateStorage } from '../../kibana_utils/public'; + +import { TimelionAppState, TimelionAppStateTransitions } from './types'; + +const STATE_STORAGE_KEY = '_a'; + +interface Arguments { + kbnUrlStateStorage: IKbnUrlStateStorage; + stateDefaults: TimelionAppState; +} + +export function initTimelionAppState({ stateDefaults, kbnUrlStateStorage }: Arguments) { + const urlState = kbnUrlStateStorage.get(STATE_STORAGE_KEY); + const initialState = { + ...stateDefaults, + ...urlState, + }; + + /* + make sure url ('_a') matches initial state + Initializing appState does two things - first it translates the defaults into AppState, + second it updates appState based on the url (the url trumps the defaults). This means if + we update the state format at all and want to handle BWC, we must not only migrate the + data stored with saved vis, but also any old state in the url. + */ + kbnUrlStateStorage.set(STATE_STORAGE_KEY, initialState, { replace: true }); + + const stateContainer = createStateContainer( + initialState, + { + set: (state) => (prop, value) => ({ ...state, [prop]: value }), + updateState: (state) => (newValues) => ({ ...state, ...newValues }), + } + ); + + const { start: startStateSync, stop: stopStateSync } = syncState({ + storageKey: STATE_STORAGE_KEY, + stateContainer: { + ...stateContainer, + set: (state) => { + if (state) { + // syncState utils requires to handle incoming "null" value + stateContainer.set(state); + } + }, + }, + stateStorage: kbnUrlStateStorage, + }); + + // start syncing the appState with the ('_a') url + startStateSync(); + + return { stateContainer, stopStateSync }; +} diff --git a/src/plugins/timelion/public/types.ts b/src/plugins/timelion/public/types.ts new file mode 100644 index 0000000000000..700485064e41b --- /dev/null +++ b/src/plugins/timelion/public/types.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export interface TimelionAppState { + sheet: string[]; + selected: number; + columns: number; + rows: number; + interval: string; +} + +export interface TimelionAppStateTransitions { + set: ( + state: TimelionAppState + ) => (prop: T, value: TimelionAppState[T]) => TimelionAppState; + updateState: ( + state: TimelionAppState + ) => (newValues: Partial) => TimelionAppState; +} diff --git a/src/plugins/timelion/public/webpackShims/jquery.flot.axislabels.js b/src/plugins/timelion/public/webpackShims/jquery.flot.axislabels.js new file mode 100644 index 0000000000000..cda8038953c76 --- /dev/null +++ b/src/plugins/timelion/public/webpackShims/jquery.flot.axislabels.js @@ -0,0 +1,462 @@ +/* +Axis Labels Plugin for flot. +http://github.com/markrcote/flot-axislabels +Original code is Copyright (c) 2010 Xuan Luo. +Original code was released under the GPLv3 license by Xuan Luo, September 2010. +Original code was rereleased under the MIT license by Xuan Luo, April 2012. +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +(function ($) { + var options = { + axisLabels: { + show: true + } + }; + + function canvasSupported() { + return !!document.createElement('canvas').getContext; + } + + function canvasTextSupported() { + if (!canvasSupported()) { + return false; + } + var dummy_canvas = document.createElement('canvas'); + var context = dummy_canvas.getContext('2d'); + return typeof context.fillText == 'function'; + } + + function css3TransitionSupported() { + var div = document.createElement('div'); + return typeof div.style.MozTransition != 'undefined' // Gecko + || typeof div.style.OTransition != 'undefined' // Opera + || typeof div.style.webkitTransition != 'undefined' // WebKit + || typeof div.style.transition != 'undefined'; + } + + + function AxisLabel(axisName, position, padding, plot, opts) { + this.axisName = axisName; + this.position = position; + this.padding = padding; + this.plot = plot; + this.opts = opts; + this.width = 0; + this.height = 0; + } + + AxisLabel.prototype.cleanup = function() { + }; + + + CanvasAxisLabel.prototype = new AxisLabel(); + CanvasAxisLabel.prototype.constructor = CanvasAxisLabel; + function CanvasAxisLabel(axisName, position, padding, plot, opts) { + AxisLabel.prototype.constructor.call(this, axisName, position, padding, + plot, opts); + } + + CanvasAxisLabel.prototype.calculateSize = function() { + if (!this.opts.axisLabelFontSizePixels) + this.opts.axisLabelFontSizePixels = 14; + if (!this.opts.axisLabelFontFamily) + this.opts.axisLabelFontFamily = 'sans-serif'; + + var textWidth = this.opts.axisLabelFontSizePixels + this.padding; + var textHeight = this.opts.axisLabelFontSizePixels + this.padding; + if (this.position == 'left' || this.position == 'right') { + this.width = this.opts.axisLabelFontSizePixels + this.padding; + this.height = 0; + } else { + this.width = 0; + this.height = this.opts.axisLabelFontSizePixels + this.padding; + } + }; + + CanvasAxisLabel.prototype.draw = function(box) { + if (!this.opts.axisLabelColour) + this.opts.axisLabelColour = 'black'; + var ctx = this.plot.getCanvas().getContext('2d'); + ctx.save(); + ctx.font = this.opts.axisLabelFontSizePixels + 'px ' + + this.opts.axisLabelFontFamily; + ctx.fillStyle = this.opts.axisLabelColour; + var width = ctx.measureText(this.opts.axisLabel).width; + var height = this.opts.axisLabelFontSizePixels; + var x, y, angle = 0; + if (this.position == 'top') { + x = box.left + box.width/2 - width/2; + y = box.top + height*0.72; + } else if (this.position == 'bottom') { + x = box.left + box.width/2 - width/2; + y = box.top + box.height - height*0.72; + } else if (this.position == 'left') { + x = box.left + height*0.72; + y = box.height/2 + box.top + width/2; + angle = -Math.PI/2; + } else if (this.position == 'right') { + x = box.left + box.width - height*0.72; + y = box.height/2 + box.top - width/2; + angle = Math.PI/2; + } + ctx.translate(x, y); + ctx.rotate(angle); + ctx.fillText(this.opts.axisLabel, 0, 0); + ctx.restore(); + }; + + + HtmlAxisLabel.prototype = new AxisLabel(); + HtmlAxisLabel.prototype.constructor = HtmlAxisLabel; + function HtmlAxisLabel(axisName, position, padding, plot, opts) { + AxisLabel.prototype.constructor.call(this, axisName, position, + padding, plot, opts); + this.elem = null; + } + + HtmlAxisLabel.prototype.calculateSize = function() { + var elem = $('
' + + this.opts.axisLabel + '
'); + this.plot.getPlaceholder().append(elem); + // store height and width of label itself, for use in draw() + this.labelWidth = elem.outerWidth(true); + this.labelHeight = elem.outerHeight(true); + elem.remove(); + + this.width = this.height = 0; + if (this.position == 'left' || this.position == 'right') { + this.width = this.labelWidth + this.padding; + } else { + this.height = this.labelHeight + this.padding; + } + }; + + HtmlAxisLabel.prototype.cleanup = function() { + if (this.elem) { + this.elem.remove(); + } + }; + + HtmlAxisLabel.prototype.draw = function(box) { + this.plot.getPlaceholder().find('#' + this.axisName + 'Label').remove(); + this.elem = $('
' + + this.opts.axisLabel + '
'); + this.plot.getPlaceholder().append(this.elem); + if (this.position == 'top') { + this.elem.css('left', box.left + box.width/2 - this.labelWidth/2 + + 'px'); + this.elem.css('top', box.top + 'px'); + } else if (this.position == 'bottom') { + this.elem.css('left', box.left + box.width/2 - this.labelWidth/2 + + 'px'); + this.elem.css('top', box.top + box.height - this.labelHeight + + 'px'); + } else if (this.position == 'left') { + this.elem.css('top', box.top + box.height/2 - this.labelHeight/2 + + 'px'); + this.elem.css('left', box.left + 'px'); + } else if (this.position == 'right') { + this.elem.css('top', box.top + box.height/2 - this.labelHeight/2 + + 'px'); + this.elem.css('left', box.left + box.width - this.labelWidth + + 'px'); + } + }; + + + CssTransformAxisLabel.prototype = new HtmlAxisLabel(); + CssTransformAxisLabel.prototype.constructor = CssTransformAxisLabel; + function CssTransformAxisLabel(axisName, position, padding, plot, opts) { + HtmlAxisLabel.prototype.constructor.call(this, axisName, position, + padding, plot, opts); + } + + CssTransformAxisLabel.prototype.calculateSize = function() { + HtmlAxisLabel.prototype.calculateSize.call(this); + this.width = this.height = 0; + if (this.position == 'left' || this.position == 'right') { + this.width = this.labelHeight + this.padding; + } else { + this.height = this.labelHeight + this.padding; + } + }; + + CssTransformAxisLabel.prototype.transforms = function(degrees, x, y) { + var stransforms = { + '-moz-transform': '', + '-webkit-transform': '', + '-o-transform': '', + '-ms-transform': '' + }; + if (x != 0 || y != 0) { + var stdTranslate = ' translate(' + x + 'px, ' + y + 'px)'; + stransforms['-moz-transform'] += stdTranslate; + stransforms['-webkit-transform'] += stdTranslate; + stransforms['-o-transform'] += stdTranslate; + stransforms['-ms-transform'] += stdTranslate; + } + if (degrees != 0) { + var rotation = degrees / 90; + var stdRotate = ' rotate(' + degrees + 'deg)'; + stransforms['-moz-transform'] += stdRotate; + stransforms['-webkit-transform'] += stdRotate; + stransforms['-o-transform'] += stdRotate; + stransforms['-ms-transform'] += stdRotate; + } + var s = 'top: 0; left: 0; '; + for (var prop in stransforms) { + if (stransforms[prop]) { + s += prop + ':' + stransforms[prop] + ';'; + } + } + s += ';'; + return s; + }; + + CssTransformAxisLabel.prototype.calculateOffsets = function(box) { + var offsets = { x: 0, y: 0, degrees: 0 }; + if (this.position == 'bottom') { + offsets.x = box.left + box.width/2 - this.labelWidth/2; + offsets.y = box.top + box.height - this.labelHeight; + } else if (this.position == 'top') { + offsets.x = box.left + box.width/2 - this.labelWidth/2; + offsets.y = box.top; + } else if (this.position == 'left') { + offsets.degrees = -90; + offsets.x = box.left - this.labelWidth/2 + this.labelHeight/2; + offsets.y = box.height/2 + box.top; + } else if (this.position == 'right') { + offsets.degrees = 90; + offsets.x = box.left + box.width - this.labelWidth/2 + - this.labelHeight/2; + offsets.y = box.height/2 + box.top; + } + offsets.x = Math.round(offsets.x); + offsets.y = Math.round(offsets.y); + + return offsets; + }; + + CssTransformAxisLabel.prototype.draw = function(box) { + this.plot.getPlaceholder().find("." + this.axisName + "Label").remove(); + var offsets = this.calculateOffsets(box); + this.elem = $('
' + this.opts.axisLabel + '
'); + this.plot.getPlaceholder().append(this.elem); + }; + + + IeTransformAxisLabel.prototype = new CssTransformAxisLabel(); + IeTransformAxisLabel.prototype.constructor = IeTransformAxisLabel; + function IeTransformAxisLabel(axisName, position, padding, plot, opts) { + CssTransformAxisLabel.prototype.constructor.call(this, axisName, + position, padding, + plot, opts); + this.requiresResize = false; + } + + IeTransformAxisLabel.prototype.transforms = function(degrees, x, y) { + // I didn't feel like learning the crazy Matrix stuff, so this uses + // a combination of the rotation transform and CSS positioning. + var s = ''; + if (degrees != 0) { + var rotation = degrees/90; + while (rotation < 0) { + rotation += 4; + } + s += ' filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=' + rotation + '); '; + // see below + this.requiresResize = (this.position == 'right'); + } + if (x != 0) { + s += 'left: ' + x + 'px; '; + } + if (y != 0) { + s += 'top: ' + y + 'px; '; + } + return s; + }; + + IeTransformAxisLabel.prototype.calculateOffsets = function(box) { + var offsets = CssTransformAxisLabel.prototype.calculateOffsets.call( + this, box); + // adjust some values to take into account differences between + // CSS and IE rotations. + if (this.position == 'top') { + // FIXME: not sure why, but placing this exactly at the top causes + // the top axis label to flip to the bottom... + offsets.y = box.top + 1; + } else if (this.position == 'left') { + offsets.x = box.left; + offsets.y = box.height/2 + box.top - this.labelWidth/2; + } else if (this.position == 'right') { + offsets.x = box.left + box.width - this.labelHeight; + offsets.y = box.height/2 + box.top - this.labelWidth/2; + } + return offsets; + }; + + IeTransformAxisLabel.prototype.draw = function(box) { + CssTransformAxisLabel.prototype.draw.call(this, box); + if (this.requiresResize) { + this.elem = this.plot.getPlaceholder().find("." + this.axisName + + "Label"); + // Since we used CSS positioning instead of transforms for + // translating the element, and since the positioning is done + // before any rotations, we have to reset the width and height + // in case the browser wrapped the text (specifically for the + // y2axis). + this.elem.css('width', this.labelWidth); + this.elem.css('height', this.labelHeight); + } + }; + + + function init(plot) { + plot.hooks.processOptions.push(function (plot, options) { + + if (!options.axisLabels.show) + return; + + // This is kind of a hack. There are no hooks in Flot between + // the creation and measuring of the ticks (setTicks, measureTickLabels + // in setupGrid() ) and the drawing of the ticks and plot box + // (insertAxisLabels in setupGrid() ). + // + // Therefore, we use a trick where we run the draw routine twice: + // the first time to get the tick measurements, so that we can change + // them, and then have it draw it again. + var secondPass = false; + + var axisLabels = {}; + var axisOffsetCounts = { left: 0, right: 0, top: 0, bottom: 0 }; + + var defaultPadding = 2; // padding between axis and tick labels + plot.hooks.draw.push(function (plot, ctx) { + var hasAxisLabels = false; + if (!secondPass) { + // MEASURE AND SET OPTIONS + $.each(plot.getAxes(), function(axisName, axis) { + var opts = axis.options // Flot 0.7 + || plot.getOptions()[axisName]; // Flot 0.6 + + // Handle redraws initiated outside of this plug-in. + if (axisName in axisLabels) { + axis.labelHeight = axis.labelHeight - + axisLabels[axisName].height; + axis.labelWidth = axis.labelWidth - + axisLabels[axisName].width; + opts.labelHeight = axis.labelHeight; + opts.labelWidth = axis.labelWidth; + axisLabels[axisName].cleanup(); + delete axisLabels[axisName]; + } + + if (!opts || !opts.axisLabel || !axis.show) + return; + + hasAxisLabels = true; + var renderer = null; + + if (!opts.axisLabelUseHtml && + navigator.appName == 'Microsoft Internet Explorer') { + var ua = navigator.userAgent; + var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); + if (re.exec(ua) != null) { + rv = parseFloat(RegExp.$1); + } + if (rv >= 9 && !opts.axisLabelUseCanvas && !opts.axisLabelUseHtml) { + renderer = CssTransformAxisLabel; + } else if (!opts.axisLabelUseCanvas && !opts.axisLabelUseHtml) { + renderer = IeTransformAxisLabel; + } else if (opts.axisLabelUseCanvas) { + renderer = CanvasAxisLabel; + } else { + renderer = HtmlAxisLabel; + } + } else { + if (opts.axisLabelUseHtml || (!css3TransitionSupported() && !canvasTextSupported()) && !opts.axisLabelUseCanvas) { + renderer = HtmlAxisLabel; + } else if (opts.axisLabelUseCanvas || !css3TransitionSupported()) { + renderer = CanvasAxisLabel; + } else { + renderer = CssTransformAxisLabel; + } + } + + var padding = opts.axisLabelPadding === undefined ? + defaultPadding : opts.axisLabelPadding; + + axisLabels[axisName] = new renderer(axisName, + axis.position, padding, + plot, opts); + + // flot interprets axis.labelHeight and .labelWidth as + // the height and width of the tick labels. We increase + // these values to make room for the axis label and + // padding. + + axisLabels[axisName].calculateSize(); + + // AxisLabel.height and .width are the size of the + // axis label and padding. + // Just set opts here because axis will be sorted out on + // the redraw. + + opts.labelHeight = axis.labelHeight + + axisLabels[axisName].height; + opts.labelWidth = axis.labelWidth + + axisLabels[axisName].width; + }); + + // If there are axis labels, re-draw with new label widths and + // heights. + + if (hasAxisLabels) { + secondPass = true; + plot.setupGrid(); + plot.draw(); + } + } else { + secondPass = false; + // DRAW + $.each(plot.getAxes(), function(axisName, axis) { + var opts = axis.options // Flot 0.7 + || plot.getOptions()[axisName]; // Flot 0.6 + if (!opts || !opts.axisLabel || !axis.show) + return; + + axisLabels[axisName].draw(axis.box); + }); + } + }); + }); + } + + + $.plot.plugins.push({ + init: init, + options: options, + name: 'axisLabels', + version: '2.0' + }); +})(jQuery); diff --git a/src/plugins/timelion/public/webpackShims/jquery.flot.crosshair.js b/src/plugins/timelion/public/webpackShims/jquery.flot.crosshair.js new file mode 100644 index 0000000000000..5111695e3d12c --- /dev/null +++ b/src/plugins/timelion/public/webpackShims/jquery.flot.crosshair.js @@ -0,0 +1,176 @@ +/* Flot plugin for showing crosshairs when the mouse hovers over the plot. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The plugin supports these options: + + crosshair: { + mode: null or "x" or "y" or "xy" + color: color + lineWidth: number + } + +Set the mode to one of "x", "y" or "xy". The "x" mode enables a vertical +crosshair that lets you trace the values on the x axis, "y" enables a +horizontal crosshair and "xy" enables them both. "color" is the color of the +crosshair (default is "rgba(170, 0, 0, 0.80)"), "lineWidth" is the width of +the drawn lines (default is 1). + +The plugin also adds four public methods: + + - setCrosshair( pos ) + + Set the position of the crosshair. Note that this is cleared if the user + moves the mouse. "pos" is in coordinates of the plot and should be on the + form { x: xpos, y: ypos } (you can use x2/x3/... if you're using multiple + axes), which is coincidentally the same format as what you get from a + "plothover" event. If "pos" is null, the crosshair is cleared. + + - clearCrosshair() + + Clear the crosshair. + + - lockCrosshair(pos) + + Cause the crosshair to lock to the current location, no longer updating if + the user moves the mouse. Optionally supply a position (passed on to + setCrosshair()) to move it to. + + Example usage: + + var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } }; + $("#graph").bind( "plothover", function ( evt, position, item ) { + if ( item ) { + // Lock the crosshair to the data point being hovered + myFlot.lockCrosshair({ + x: item.datapoint[ 0 ], + y: item.datapoint[ 1 ] + }); + } else { + // Return normal crosshair operation + myFlot.unlockCrosshair(); + } + }); + + - unlockCrosshair() + + Free the crosshair to move again after locking it. +*/ + +(function ($) { + var options = { + crosshair: { + mode: null, // one of null, "x", "y" or "xy", + color: "rgba(170, 0, 0, 0.80)", + lineWidth: 1 + } + }; + + function init(plot) { + // position of crosshair in pixels + var crosshair = { x: -1, y: -1, locked: false }; + + plot.setCrosshair = function setCrosshair(pos) { + if (!pos) + crosshair.x = -1; + else { + var o = plot.p2c(pos); + crosshair.x = Math.max(0, Math.min(o.left, plot.width())); + crosshair.y = Math.max(0, Math.min(o.top, plot.height())); + } + + plot.triggerRedrawOverlay(); + }; + + plot.clearCrosshair = plot.setCrosshair; // passes null for pos + + plot.lockCrosshair = function lockCrosshair(pos) { + if (pos) + plot.setCrosshair(pos); + crosshair.locked = true; + }; + + plot.unlockCrosshair = function unlockCrosshair() { + crosshair.locked = false; + }; + + function onMouseOut(e) { + if (crosshair.locked) + return; + + if (crosshair.x != -1) { + crosshair.x = -1; + plot.triggerRedrawOverlay(); + } + } + + function onMouseMove(e) { + if (crosshair.locked) + return; + + if (plot.getSelection && plot.getSelection()) { + crosshair.x = -1; // hide the crosshair while selecting + return; + } + + var offset = plot.offset(); + crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width())); + crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height())); + plot.triggerRedrawOverlay(); + } + + plot.hooks.bindEvents.push(function (plot, eventHolder) { + if (!plot.getOptions().crosshair.mode) + return; + + eventHolder.mouseout(onMouseOut); + eventHolder.mousemove(onMouseMove); + }); + + plot.hooks.drawOverlay.push(function (plot, ctx) { + var c = plot.getOptions().crosshair; + if (!c.mode) + return; + + var plotOffset = plot.getPlotOffset(); + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + if (crosshair.x != -1) { + var adj = plot.getOptions().crosshair.lineWidth % 2 ? 0.5 : 0; + + ctx.strokeStyle = c.color; + ctx.lineWidth = c.lineWidth; + ctx.lineJoin = "round"; + + ctx.beginPath(); + if (c.mode.indexOf("x") != -1) { + var drawX = Math.floor(crosshair.x) + adj; + ctx.moveTo(drawX, 0); + ctx.lineTo(drawX, plot.height()); + } + if (c.mode.indexOf("y") != -1) { + var drawY = Math.floor(crosshair.y) + adj; + ctx.moveTo(0, drawY); + ctx.lineTo(plot.width(), drawY); + } + ctx.stroke(); + } + ctx.restore(); + }); + + plot.hooks.shutdown.push(function (plot, eventHolder) { + eventHolder.unbind("mouseout", onMouseOut); + eventHolder.unbind("mousemove", onMouseMove); + }); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'crosshair', + version: '1.0' + }); +})(jQuery); diff --git a/src/plugins/timelion/public/webpackShims/jquery.flot.js b/src/plugins/timelion/public/webpackShims/jquery.flot.js new file mode 100644 index 0000000000000..5d613037cf234 --- /dev/null +++ b/src/plugins/timelion/public/webpackShims/jquery.flot.js @@ -0,0 +1,3168 @@ +/* JavaScript plotting library for jQuery, version 0.8.3. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +*/ + +// first an inline dependency, jquery.colorhelpers.js, we inline it here +// for convenience + +/* Plugin for jQuery for working with colors. + * + * Version 1.1. + * + * Inspiration from jQuery color animation plugin by John Resig. + * + * Released under the MIT license by Ole Laursen, October 2009. + * + * Examples: + * + * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() + * var c = $.color.extract($("#mydiv"), 'background-color'); + * console.log(c.r, c.g, c.b, c.a); + * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" + * + * Note that .scale() and .add() return the same modified object + * instead of making a new one. + * + * V. 1.1: Fix error handling so e.g. parsing an empty string does + * produce a color rather than just crashing. + */ +(function($){$.color={};$.color.make=function(r,g,b,a){var o={};o.r=r||0;o.g=g||0;o.b=b||0;o.a=a!=null?a:1;o.add=function(c,d){for(var i=0;i=1){return"rgb("+[o.r,o.g,o.b].join(",")+")"}else{return"rgba("+[o.r,o.g,o.b,o.a].join(",")+")"}};o.normalize=function(){function clamp(min,value,max){return valuemax?max:value}o.r=clamp(0,parseInt(o.r),255);o.g=clamp(0,parseInt(o.g),255);o.b=clamp(0,parseInt(o.b),255);o.a=clamp(0,o.a,1);return o};o.clone=function(){return $.color.make(o.r,o.b,o.g,o.a)};return o.normalize()};$.color.extract=function(elem,css){var c;do{c=elem.css(css).toLowerCase();if(c!=""&&c!="transparent")break;elem=elem.parent()}while(elem.length&&!$.nodeName(elem.get(0),"body"));if(c=="rgba(0, 0, 0, 0)")c="transparent";return $.color.parse(c)};$.color.parse=function(str){var res,m=$.color.make;if(res=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10));if(res=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10),parseFloat(res[4]));if(res=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55);if(res=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55,parseFloat(res[4]));if(res=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))return m(parseInt(res[1],16),parseInt(res[2],16),parseInt(res[3],16));if(res=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))return m(parseInt(res[1]+res[1],16),parseInt(res[2]+res[2],16),parseInt(res[3]+res[3],16));var name=$.trim(str).toLowerCase();if(name=="transparent")return m(255,255,255,0);else{res=lookupColors[name]||[0,0,0];return m(res[0],res[1],res[2])}};var lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery); + +// the actual Flot code +(function($) { + + // Cache the prototype hasOwnProperty for faster access + + var hasOwnProperty = Object.prototype.hasOwnProperty; + + // A shim to provide 'detach' to jQuery versions prior to 1.4. Using a DOM + // operation produces the same effect as detach, i.e. removing the element + // without touching its jQuery data. + + // Do not merge this into Flot 0.9, since it requires jQuery 1.4.4+. + + if (!$.fn.detach) { + $.fn.detach = function() { + return this.each(function() { + if (this.parentNode) { + this.parentNode.removeChild( this ); + } + }); + }; + } + + /////////////////////////////////////////////////////////////////////////// + // The Canvas object is a wrapper around an HTML5 tag. + // + // @constructor + // @param {string} cls List of classes to apply to the canvas. + // @param {element} container Element onto which to append the canvas. + // + // Requiring a container is a little iffy, but unfortunately canvas + // operations don't work unless the canvas is attached to the DOM. + + function Canvas(cls, container) { + + var element = container.children("." + cls)[0]; + + if (element == null) { + + element = document.createElement("canvas"); + element.className = cls; + + $(element).css({ direction: "ltr", position: "absolute", left: 0, top: 0 }) + .appendTo(container); + + // If HTML5 Canvas isn't available, fall back to [Ex|Flash]canvas + + if (!element.getContext) { + if (window.G_vmlCanvasManager) { + element = window.G_vmlCanvasManager.initElement(element); + } else { + throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode."); + } + } + } + + this.element = element; + + var context = this.context = element.getContext("2d"); + + // Determine the screen's ratio of physical to device-independent + // pixels. This is the ratio between the canvas width that the browser + // advertises and the number of pixels actually present in that space. + + // The iPhone 4, for example, has a device-independent width of 320px, + // but its screen is actually 640px wide. It therefore has a pixel + // ratio of 2, while most normal devices have a ratio of 1. + + var devicePixelRatio = window.devicePixelRatio || 1, + backingStoreRatio = + context.webkitBackingStorePixelRatio || + context.mozBackingStorePixelRatio || + context.msBackingStorePixelRatio || + context.oBackingStorePixelRatio || + context.backingStorePixelRatio || 1; + + this.pixelRatio = devicePixelRatio / backingStoreRatio; + + // Size the canvas to match the internal dimensions of its container + + this.resize(container.width(), container.height()); + + // Collection of HTML div layers for text overlaid onto the canvas + + this.textContainer = null; + this.text = {}; + + // Cache of text fragments and metrics, so we can avoid expensively + // re-calculating them when the plot is re-rendered in a loop. + + this._textCache = {}; + } + + // Resizes the canvas to the given dimensions. + // + // @param {number} width New width of the canvas, in pixels. + // @param {number} width New height of the canvas, in pixels. + + Canvas.prototype.resize = function(width, height) { + + if (width <= 0 || height <= 0) { + throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + height); + } + + var element = this.element, + context = this.context, + pixelRatio = this.pixelRatio; + + // Resize the canvas, increasing its density based on the display's + // pixel ratio; basically giving it more pixels without increasing the + // size of its element, to take advantage of the fact that retina + // displays have that many more pixels in the same advertised space. + + // Resizing should reset the state (excanvas seems to be buggy though) + + if (this.width != width) { + element.width = width * pixelRatio; + element.style.width = width + "px"; + this.width = width; + } + + if (this.height != height) { + element.height = height * pixelRatio; + element.style.height = height + "px"; + this.height = height; + } + + // Save the context, so we can reset in case we get replotted. The + // restore ensure that we're really back at the initial state, and + // should be safe even if we haven't saved the initial state yet. + + context.restore(); + context.save(); + + // Scale the coordinate space to match the display density; so even though we + // may have twice as many pixels, we still want lines and other drawing to + // appear at the same size; the extra pixels will just make them crisper. + + context.scale(pixelRatio, pixelRatio); + }; + + // Clears the entire canvas area, not including any overlaid HTML text + + Canvas.prototype.clear = function() { + this.context.clearRect(0, 0, this.width, this.height); + }; + + // Finishes rendering the canvas, including managing the text overlay. + + Canvas.prototype.render = function() { + + var cache = this._textCache; + + // For each text layer, add elements marked as active that haven't + // already been rendered, and remove those that are no longer active. + + for (var layerKey in cache) { + if (hasOwnProperty.call(cache, layerKey)) { + + var layer = this.getTextLayer(layerKey), + layerCache = cache[layerKey]; + + layer.hide(); + + for (var styleKey in layerCache) { + if (hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey]; + for (var key in styleCache) { + if (hasOwnProperty.call(styleCache, key)) { + + var positions = styleCache[key].positions; + + for (var i = 0, position; position = positions[i]; i++) { + if (position.active) { + if (!position.rendered) { + layer.append(position.element); + position.rendered = true; + } + } else { + positions.splice(i--, 1); + if (position.rendered) { + position.element.detach(); + } + } + } + + if (positions.length == 0) { + delete styleCache[key]; + } + } + } + } + } + + layer.show(); + } + } + }; + + // Creates (if necessary) and returns the text overlay container. + // + // @param {string} classes String of space-separated CSS classes used to + // uniquely identify the text layer. + // @return {object} The jQuery-wrapped text-layer div. + + Canvas.prototype.getTextLayer = function(classes) { + + var layer = this.text[classes]; + + // Create the text layer if it doesn't exist + + if (layer == null) { + + // Create the text layer container, if it doesn't exist + + if (this.textContainer == null) { + this.textContainer = $("
") + .css({ + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0, + 'font-size': "smaller", + color: "#545454" + }) + .insertAfter(this.element); + } + + layer = this.text[classes] = $("
") + .addClass(classes) + .css({ + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0 + }) + .appendTo(this.textContainer); + } + + return layer; + }; + + // Creates (if necessary) and returns a text info object. + // + // The object looks like this: + // + // { + // width: Width of the text's wrapper div. + // height: Height of the text's wrapper div. + // element: The jQuery-wrapped HTML div containing the text. + // positions: Array of positions at which this text is drawn. + // } + // + // The positions array contains objects that look like this: + // + // { + // active: Flag indicating whether the text should be visible. + // rendered: Flag indicating whether the text is currently visible. + // element: The jQuery-wrapped HTML div containing the text. + // x: X coordinate at which to draw the text. + // y: Y coordinate at which to draw the text. + // } + // + // Each position after the first receives a clone of the original element. + // + // The idea is that that the width, height, and general 'identity' of the + // text is constant no matter where it is placed; the placements are a + // secondary property. + // + // Canvas maintains a cache of recently-used text info objects; getTextInfo + // either returns the cached element or creates a new entry. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {string} text Text string to retrieve info for. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which to rotate the text, in degrees. + // Angle is currently unused, it will be implemented in the future. + // @param {number=} width Maximum width of the text before it wraps. + // @return {object} a text info object. + + Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { + + var textStyle, layerCache, styleCache, info; + + // Cast the value to a string, in case we were given a number or such + + text = "" + text; + + // If the font is a font-spec object, generate a CSS font definition + + if (typeof font === "object") { + textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px/" + font.lineHeight + "px " + font.family; + } else { + textStyle = font; + } + + // Retrieve (or create) the cache for the text's layer and styles + + layerCache = this._textCache[layer]; + + if (layerCache == null) { + layerCache = this._textCache[layer] = {}; + } + + styleCache = layerCache[textStyle]; + + if (styleCache == null) { + styleCache = layerCache[textStyle] = {}; + } + + info = styleCache[text]; + + // If we can't find a matching element in our cache, create a new one + + if (info == null) { + + var element = $("
").html(text) + .css({ + position: "absolute", + 'max-width': width, + top: -9999 + }) + .appendTo(this.getTextLayer(layer)); + + if (typeof font === "object") { + element.css({ + font: textStyle, + color: font.color + }); + } else if (typeof font === "string") { + element.addClass(font); + } + + info = styleCache[text] = { + width: element.outerWidth(true), + height: element.outerHeight(true), + element: element, + positions: [] + }; + + element.detach(); + } + + return info; + }; + + // Adds a text string to the canvas text overlay. + // + // The text isn't drawn immediately; it is marked as rendering, which will + // result in its addition to the canvas on the next render pass. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {number} x X coordinate at which to draw the text. + // @param {number} y Y coordinate at which to draw the text. + // @param {string} text Text string to draw. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which to rotate the text, in degrees. + // Angle is currently unused, it will be implemented in the future. + // @param {number=} width Maximum width of the text before it wraps. + // @param {string=} halign Horizontal alignment of the text; either "left", + // "center" or "right". + // @param {string=} valign Vertical alignment of the text; either "top", + // "middle" or "bottom". + + Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { + + var info = this.getTextInfo(layer, text, font, angle, width), + positions = info.positions; + + // Tweak the div's position to match the text's alignment + + if (halign == "center") { + x -= info.width / 2; + } else if (halign == "right") { + x -= info.width; + } + + if (valign == "middle") { + y -= info.height / 2; + } else if (valign == "bottom") { + y -= info.height; + } + + // Determine whether this text already exists at this position. + // If so, mark it for inclusion in the next render pass. + + for (var i = 0, position; position = positions[i]; i++) { + if (position.x == x && position.y == y) { + position.active = true; + return; + } + } + + // If the text doesn't exist at this position, create a new entry + + // For the very first position we'll re-use the original element, + // while for subsequent ones we'll clone it. + + position = { + active: true, + rendered: false, + element: positions.length ? info.element.clone() : info.element, + x: x, + y: y + }; + + positions.push(position); + + // Move the element to its final position within the container + + position.element.css({ + top: Math.round(y), + left: Math.round(x), + 'text-align': halign // In case the text wraps + }); + }; + + // Removes one or more text strings from the canvas text overlay. + // + // If no parameters are given, all text within the layer is removed. + // + // Note that the text is not immediately removed; it is simply marked as + // inactive, which will result in its removal on the next render pass. + // This avoids the performance penalty for 'clear and redraw' behavior, + // where we potentially get rid of all text on a layer, but will likely + // add back most or all of it later, as when redrawing axes, for example. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {number=} x X coordinate of the text. + // @param {number=} y Y coordinate of the text. + // @param {string=} text Text string to remove. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which the text is rotated, in degrees. + // Angle is currently unused, it will be implemented in the future. + + Canvas.prototype.removeText = function(layer, x, y, text, font, angle) { + if (text == null) { + var layerCache = this._textCache[layer]; + if (layerCache != null) { + for (var styleKey in layerCache) { + if (hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey]; + for (var key in styleCache) { + if (hasOwnProperty.call(styleCache, key)) { + var positions = styleCache[key].positions; + for (var i = 0, position; position = positions[i]; i++) { + position.active = false; + } + } + } + } + } + } + } else { + var positions = this.getTextInfo(layer, text, font, angle).positions; + for (var i = 0, position; position = positions[i]; i++) { + if (position.x == x && position.y == y) { + position.active = false; + } + } + } + }; + + /////////////////////////////////////////////////////////////////////////// + // The top-level container for the entire plot. + + function Plot(placeholder, data_, options_, plugins) { + // data is on the form: + // [ series1, series2 ... ] + // where series is either just the data as [ [x1, y1], [x2, y2], ... ] + // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... } + + var series = [], + options = { + // the color theme used for graphs + colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], + legend: { + show: true, + noColumns: 1, // number of columns in legend table + labelFormatter: null, // fn: string -> string + labelBoxBorderColor: "#ccc", // border color for the little label boxes + container: null, // container (as jQuery object) to put legend in, null means default on top of graph + position: "ne", // position of default legend container within plot + margin: 5, // distance from grid edge to default legend container within plot + backgroundColor: null, // null means auto-detect + backgroundOpacity: 0.85, // set to 0 to avoid background + sorted: null // default to no legend sorting + }, + xaxis: { + show: null, // null = auto-detect, true = always, false = never + position: "bottom", // or "top" + mode: null, // null or "time" + font: null, // null (derived from CSS in placeholder) or object like { size: 11, lineHeight: 13, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" } + color: null, // base color, labels, ticks + tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)" + transform: null, // null or f: number -> number to transform axis + inverseTransform: null, // if transform is set, this should be the inverse function + min: null, // min. value to show, null means set automatically + max: null, // max. value to show, null means set automatically + autoscaleMargin: null, // margin in % to add if auto-setting min/max + ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks + tickFormatter: null, // fn: number -> string + labelWidth: null, // size of tick labels in pixels + labelHeight: null, + reserveSpace: null, // whether to reserve space even if axis isn't shown + tickLength: null, // size in pixels of ticks, or "full" for whole line + alignTicksWithAxis: null, // axis number or null for no sync + tickDecimals: null, // no. of decimals, null means auto + tickSize: null, // number or [number, "unit"] + minTickSize: null // number or [number, "unit"] + }, + yaxis: { + autoscaleMargin: 0.02, + position: "left" // or "right" + }, + xaxes: [], + yaxes: [], + series: { + points: { + show: false, + radius: 3, + lineWidth: 2, // in pixels + fill: true, + fillColor: "#ffffff", + symbol: "circle" // or callback + }, + lines: { + // we don't put in show: false so we can see + // whether lines were actively disabled + lineWidth: 2, // in pixels + fill: false, + fillColor: null, + steps: false + // Omit 'zero', so we can later default its value to + // match that of the 'fill' option. + }, + bars: { + show: false, + lineWidth: 2, // in pixels + barWidth: 1, // in units of the x axis + fill: true, + fillColor: null, + align: "left", // "left", "right", or "center" + horizontal: false, + zero: true + }, + shadowSize: 3, + highlightColor: null + }, + grid: { + show: true, + aboveData: false, + color: "#545454", // primary color used for outline and labels + backgroundColor: null, // null for transparent, else color + borderColor: null, // set if different from the grid color + tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)" + margin: 0, // distance from the canvas edge to the grid + labelMargin: 5, // in pixels + axisMargin: 8, // in pixels + borderWidth: 2, // in pixels + minBorderMargin: null, // in pixels, null means taken from points radius + markings: null, // array of ranges or fn: axes -> array of ranges + markingsColor: "#f4f4f4", + markingsLineWidth: 2, + // interactive stuff + clickable: false, + hoverable: false, + autoHighlight: true, // highlight in case mouse is near + mouseActiveRadius: 10 // how far the mouse can be away to activate an item + }, + interaction: { + redrawOverlayInterval: 1000/60 // time between updates, -1 means in same flow + }, + hooks: {} + }, + surface = null, // the canvas for the plot itself + overlay = null, // canvas for interactive stuff on top of plot + eventHolder = null, // jQuery object that events should be bound to + ctx = null, octx = null, + xaxes = [], yaxes = [], + plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, + plotWidth = 0, plotHeight = 0, + hooks = { + processOptions: [], + processRawData: [], + processDatapoints: [], + processOffset: [], + drawBackground: [], + drawSeries: [], + draw: [], + bindEvents: [], + drawOverlay: [], + shutdown: [] + }, + plot = this; + + // public functions + plot.setData = setData; + plot.setupGrid = setupGrid; + plot.draw = draw; + plot.getPlaceholder = function() { return placeholder; }; + plot.getCanvas = function() { return surface.element; }; + plot.getPlotOffset = function() { return plotOffset; }; + plot.width = function () { return plotWidth; }; + plot.height = function () { return plotHeight; }; + plot.offset = function () { + var o = eventHolder.offset(); + o.left += plotOffset.left; + o.top += plotOffset.top; + return o; + }; + plot.getData = function () { return series; }; + plot.getAxes = function () { + var res = {}, i; + $.each(xaxes.concat(yaxes), function (_, axis) { + if (axis) + res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis; + }); + return res; + }; + plot.getXAxes = function () { return xaxes; }; + plot.getYAxes = function () { return yaxes; }; + plot.c2p = canvasToAxisCoords; + plot.p2c = axisToCanvasCoords; + plot.getOptions = function () { return options; }; + plot.highlight = highlight; + plot.unhighlight = unhighlight; + plot.triggerRedrawOverlay = triggerRedrawOverlay; + plot.pointOffset = function(point) { + return { + left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left, 10), + top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top, 10) + }; + }; + plot.shutdown = shutdown; + plot.destroy = function () { + shutdown(); + placeholder.removeData("plot").empty(); + + series = []; + options = null; + surface = null; + overlay = null; + eventHolder = null; + ctx = null; + octx = null; + xaxes = []; + yaxes = []; + hooks = null; + highlights = []; + plot = null; + }; + plot.resize = function () { + var width = placeholder.width(), + height = placeholder.height(); + surface.resize(width, height); + overlay.resize(width, height); + }; + + // public attributes + plot.hooks = hooks; + + // initialize + initPlugins(plot); + parseOptions(options_); + setupCanvases(); + setData(data_); + setupGrid(); + draw(); + bindEvents(); + + + function executeHooks(hook, args) { + args = [plot].concat(args); + for (var i = 0; i < hook.length; ++i) + hook[i].apply(this, args); + } + + function initPlugins() { + + // References to key classes, allowing plugins to modify them + + var classes = { + Canvas: Canvas + }; + + for (var i = 0; i < plugins.length; ++i) { + var p = plugins[i]; + p.init(plot, classes); + if (p.options) + $.extend(true, options, p.options); + } + } + + function parseOptions(opts) { + + $.extend(true, options, opts); + + // $.extend merges arrays, rather than replacing them. When less + // colors are provided than the size of the default palette, we + // end up with those colors plus the remaining defaults, which is + // not expected behavior; avoid it by replacing them here. + + if (opts && opts.colors) { + options.colors = opts.colors; + } + + if (options.xaxis.color == null) + options.xaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + if (options.yaxis.color == null) + options.yaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + + if (options.xaxis.tickColor == null) // grid.tickColor for back-compatibility + options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color; + if (options.yaxis.tickColor == null) // grid.tickColor for back-compatibility + options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color; + + if (options.grid.borderColor == null) + options.grid.borderColor = options.grid.color; + if (options.grid.tickColor == null) + options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + + // Fill in defaults for axis options, including any unspecified + // font-spec fields, if a font-spec was provided. + + // If no x/y axis options were provided, create one of each anyway, + // since the rest of the code assumes that they exist. + + var i, axisOptions, axisCount, + fontSize = placeholder.css("font-size"), + fontSizeDefault = fontSize ? +fontSize.replace("px", "") : 13, + fontDefaults = { + style: placeholder.css("font-style"), + size: Math.round(0.8 * fontSizeDefault), + variant: placeholder.css("font-variant"), + weight: placeholder.css("font-weight"), + family: placeholder.css("font-family") + }; + + axisCount = options.xaxes.length || 1; + for (i = 0; i < axisCount; ++i) { + + axisOptions = options.xaxes[i]; + if (axisOptions && !axisOptions.tickColor) { + axisOptions.tickColor = axisOptions.color; + } + + axisOptions = $.extend(true, {}, options.xaxis, axisOptions); + options.xaxes[i] = axisOptions; + + if (axisOptions.font) { + axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); + if (!axisOptions.font.color) { + axisOptions.font.color = axisOptions.color; + } + if (!axisOptions.font.lineHeight) { + axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); + } + } + } + + axisCount = options.yaxes.length || 1; + for (i = 0; i < axisCount; ++i) { + + axisOptions = options.yaxes[i]; + if (axisOptions && !axisOptions.tickColor) { + axisOptions.tickColor = axisOptions.color; + } + + axisOptions = $.extend(true, {}, options.yaxis, axisOptions); + options.yaxes[i] = axisOptions; + + if (axisOptions.font) { + axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); + if (!axisOptions.font.color) { + axisOptions.font.color = axisOptions.color; + } + if (!axisOptions.font.lineHeight) { + axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); + } + } + } + + // backwards compatibility, to be removed in future + if (options.xaxis.noTicks && options.xaxis.ticks == null) + options.xaxis.ticks = options.xaxis.noTicks; + if (options.yaxis.noTicks && options.yaxis.ticks == null) + options.yaxis.ticks = options.yaxis.noTicks; + if (options.x2axis) { + options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis); + options.xaxes[1].position = "top"; + // Override the inherit to allow the axis to auto-scale + if (options.x2axis.min == null) { + options.xaxes[1].min = null; + } + if (options.x2axis.max == null) { + options.xaxes[1].max = null; + } + } + if (options.y2axis) { + options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis); + options.yaxes[1].position = "right"; + // Override the inherit to allow the axis to auto-scale + if (options.y2axis.min == null) { + options.yaxes[1].min = null; + } + if (options.y2axis.max == null) { + options.yaxes[1].max = null; + } + } + if (options.grid.coloredAreas) + options.grid.markings = options.grid.coloredAreas; + if (options.grid.coloredAreasColor) + options.grid.markingsColor = options.grid.coloredAreasColor; + if (options.lines) + $.extend(true, options.series.lines, options.lines); + if (options.points) + $.extend(true, options.series.points, options.points); + if (options.bars) + $.extend(true, options.series.bars, options.bars); + if (options.shadowSize != null) + options.series.shadowSize = options.shadowSize; + if (options.highlightColor != null) + options.series.highlightColor = options.highlightColor; + + // save options on axes for future reference + for (i = 0; i < options.xaxes.length; ++i) + getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i]; + for (i = 0; i < options.yaxes.length; ++i) + getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i]; + + // add hooks from options + for (var n in hooks) + if (options.hooks[n] && options.hooks[n].length) + hooks[n] = hooks[n].concat(options.hooks[n]); + + executeHooks(hooks.processOptions, [options]); + } + + function setData(d) { + series = parseData(d); + fillInSeriesOptions(); + processData(); + } + + function parseData(d) { + var res = []; + for (var i = 0; i < d.length; ++i) { + var s = $.extend(true, {}, options.series); + + if (d[i].data != null) { + s.data = d[i].data; // move the data instead of deep-copy + delete d[i].data; + + $.extend(true, s, d[i]); + + d[i].data = s.data; + } + else + s.data = d[i]; + res.push(s); + } + + return res; + } + + function axisNumber(obj, coord) { + var a = obj[coord + "axis"]; + if (typeof a == "object") // if we got a real axis, extract number + a = a.n; + if (typeof a != "number") + a = 1; // default to first axis + return a; + } + + function allAxes() { + // return flat array without annoying null entries + return $.grep(xaxes.concat(yaxes), function (a) { return a; }); + } + + function canvasToAxisCoords(pos) { + // return an object with x/y corresponding to all used axes + var res = {}, i, axis; + for (i = 0; i < xaxes.length; ++i) { + axis = xaxes[i]; + if (axis && axis.used) + res["x" + axis.n] = axis.c2p(pos.left); + } + + for (i = 0; i < yaxes.length; ++i) { + axis = yaxes[i]; + if (axis && axis.used) + res["y" + axis.n] = axis.c2p(pos.top); + } + + if (res.x1 !== undefined) + res.x = res.x1; + if (res.y1 !== undefined) + res.y = res.y1; + + return res; + } + + function axisToCanvasCoords(pos) { + // get canvas coords from the first pair of x/y found in pos + var res = {}, i, axis, key; + + for (i = 0; i < xaxes.length; ++i) { + axis = xaxes[i]; + if (axis && axis.used) { + key = "x" + axis.n; + if (pos[key] == null && axis.n == 1) + key = "x"; + + if (pos[key] != null) { + res.left = axis.p2c(pos[key]); + break; + } + } + } + + for (i = 0; i < yaxes.length; ++i) { + axis = yaxes[i]; + if (axis && axis.used) { + key = "y" + axis.n; + if (pos[key] == null && axis.n == 1) + key = "y"; + + if (pos[key] != null) { + res.top = axis.p2c(pos[key]); + break; + } + } + } + + return res; + } + + function getOrCreateAxis(axes, number) { + if (!axes[number - 1]) + axes[number - 1] = { + n: number, // save the number for future reference + direction: axes == xaxes ? "x" : "y", + options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis) + }; + + return axes[number - 1]; + } + + function fillInSeriesOptions() { + + var neededColors = series.length, maxIndex = -1, i; + + // Subtract the number of series that already have fixed colors or + // color indexes from the number that we still need to generate. + + for (i = 0; i < series.length; ++i) { + var sc = series[i].color; + if (sc != null) { + neededColors--; + if (typeof sc == "number" && sc > maxIndex) { + maxIndex = sc; + } + } + } + + // If any of the series have fixed color indexes, then we need to + // generate at least as many colors as the highest index. + + if (neededColors <= maxIndex) { + neededColors = maxIndex + 1; + } + + // Generate all the colors, using first the option colors and then + // variations on those colors once they're exhausted. + + var c, colors = [], colorPool = options.colors, + colorPoolSize = colorPool.length, variation = 0; + + for (i = 0; i < neededColors; i++) { + + c = $.color.parse(colorPool[i % colorPoolSize] || "#666"); + + // Each time we exhaust the colors in the pool we adjust + // a scaling factor used to produce more variations on + // those colors. The factor alternates negative/positive + // to produce lighter/darker colors. + + // Reset the variation after every few cycles, or else + // it will end up producing only white or black colors. + + if (i % colorPoolSize == 0 && i) { + if (variation >= 0) { + if (variation < 0.5) { + variation = -variation - 0.2; + } else variation = 0; + } else variation = -variation; + } + + colors[i] = c.scale('rgb', 1 + variation); + } + + // Finalize the series options, filling in their colors + + var colori = 0, s; + for (i = 0; i < series.length; ++i) { + s = series[i]; + + // assign colors + if (s.color == null) { + s.color = colors[colori].toString(); + ++colori; + } + else if (typeof s.color == "number") + s.color = colors[s.color].toString(); + + // turn on lines automatically in case nothing is set + if (s.lines.show == null) { + var v, show = true; + for (v in s) + if (s[v] && s[v].show) { + show = false; + break; + } + if (show) + s.lines.show = true; + } + + // If nothing was provided for lines.zero, default it to match + // lines.fill, since areas by default should extend to zero. + + if (s.lines.zero == null) { + s.lines.zero = !!s.lines.fill; + } + + // setup axes + s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x")); + s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y")); + } + } + + function processData() { + var topSentry = Number.POSITIVE_INFINITY, + bottomSentry = Number.NEGATIVE_INFINITY, + fakeInfinity = Number.MAX_VALUE, + i, j, k, m, length, + s, points, ps, x, y, axis, val, f, p, + data, format; + + function updateAxis(axis, min, max) { + if (min < axis.datamin && min != -fakeInfinity) + axis.datamin = min; + if (max > axis.datamax && max != fakeInfinity) + axis.datamax = max; + } + + $.each(allAxes(), function (_, axis) { + // init axis + axis.datamin = topSentry; + axis.datamax = bottomSentry; + axis.used = false; + }); + + for (i = 0; i < series.length; ++i) { + s = series[i]; + s.datapoints = { points: [] }; + + executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]); + } + + // first pass: clean and copy data + for (i = 0; i < series.length; ++i) { + s = series[i]; + + data = s.data; + format = s.datapoints.format; + + if (!format) { + format = []; + // find out how to copy + format.push({ x: true, number: true, required: true }); + format.push({ y: true, number: true, required: true }); + + if (s.bars.show || (s.lines.show && s.lines.fill)) { + var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); + format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale }); + if (s.bars.horizontal) { + delete format[format.length - 1].y; + format[format.length - 1].x = true; + } + } + + s.datapoints.format = format; + } + + if (s.datapoints.pointsize != null) + continue; // already filled in + + s.datapoints.pointsize = format.length; + + ps = s.datapoints.pointsize; + points = s.datapoints.points; + + var insertSteps = s.lines.show && s.lines.steps; + s.xaxis.used = s.yaxis.used = true; + + for (j = k = 0; j < data.length; ++j, k += ps) { + p = data[j]; + + var nullify = p == null; + if (!nullify) { + for (m = 0; m < ps; ++m) { + val = p[m]; + f = format[m]; + + if (f) { + if (f.number && val != null) { + val = +val; // convert to number + if (isNaN(val)) + val = null; + else if (val == Infinity) + val = fakeInfinity; + else if (val == -Infinity) + val = -fakeInfinity; + } + + if (val == null) { + if (f.required) + nullify = true; + + if (f.defaultValue != null) + val = f.defaultValue; + } + } + + points[k + m] = val; + } + } + + if (nullify) { + for (m = 0; m < ps; ++m) { + val = points[k + m]; + if (val != null) { + f = format[m]; + // extract min/max info + if (f.autoscale !== false) { + if (f.x) { + updateAxis(s.xaxis, val, val); + } + if (f.y) { + updateAxis(s.yaxis, val, val); + } + } + } + points[k + m] = null; + } + } + else { + // a little bit of line specific stuff that + // perhaps shouldn't be here, but lacking + // better means... + if (insertSteps && k > 0 + && points[k - ps] != null + && points[k - ps] != points[k] + && points[k - ps + 1] != points[k + 1]) { + // copy the point to make room for a middle point + for (m = 0; m < ps; ++m) + points[k + ps + m] = points[k + m]; + + // middle point has same y + points[k + 1] = points[k - ps + 1]; + + // we've added a point, better reflect that + k += ps; + } + } + } + } + + // give the hooks a chance to run + for (i = 0; i < series.length; ++i) { + s = series[i]; + + executeHooks(hooks.processDatapoints, [ s, s.datapoints]); + } + + // second pass: find datamax/datamin for auto-scaling + for (i = 0; i < series.length; ++i) { + s = series[i]; + points = s.datapoints.points; + ps = s.datapoints.pointsize; + format = s.datapoints.format; + + var xmin = topSentry, ymin = topSentry, + xmax = bottomSentry, ymax = bottomSentry; + + for (j = 0; j < points.length; j += ps) { + if (points[j] == null) + continue; + + for (m = 0; m < ps; ++m) { + val = points[j + m]; + f = format[m]; + if (!f || f.autoscale === false || val == fakeInfinity || val == -fakeInfinity) + continue; + + if (f.x) { + if (val < xmin) + xmin = val; + if (val > xmax) + xmax = val; + } + if (f.y) { + if (val < ymin) + ymin = val; + if (val > ymax) + ymax = val; + } + } + } + + if (s.bars.show) { + // make sure we got room for the bar on the dancing floor + var delta; + + switch (s.bars.align) { + case "left": + delta = 0; + break; + case "right": + delta = -s.bars.barWidth; + break; + default: + delta = -s.bars.barWidth / 2; + } + + if (s.bars.horizontal) { + ymin += delta; + ymax += delta + s.bars.barWidth; + } + else { + xmin += delta; + xmax += delta + s.bars.barWidth; + } + } + + updateAxis(s.xaxis, xmin, xmax); + updateAxis(s.yaxis, ymin, ymax); + } + + $.each(allAxes(), function (_, axis) { + if (axis.datamin == topSentry) + axis.datamin = null; + if (axis.datamax == bottomSentry) + axis.datamax = null; + }); + } + + function setupCanvases() { + + // Make sure the placeholder is clear of everything except canvases + // from a previous plot in this container that we'll try to re-use. + + placeholder.css("padding", 0) // padding messes up the positioning + .children().filter(function(){ + return !$(this).hasClass("flot-overlay") && !$(this).hasClass('flot-base'); + }).remove(); + + if (placeholder.css("position") == 'static') + placeholder.css("position", "relative"); // for positioning labels and overlay + + surface = new Canvas("flot-base", placeholder); + overlay = new Canvas("flot-overlay", placeholder); // overlay canvas for interactive features + + ctx = surface.context; + octx = overlay.context; + + // define which element we're listening for events on + eventHolder = $(overlay.element).unbind(); + + // If we're re-using a plot object, shut down the old one + + var existing = placeholder.data("plot"); + + if (existing) { + existing.shutdown(); + overlay.clear(); + } + + // save in case we get replotted + placeholder.data("plot", plot); + } + + function bindEvents() { + // bind events + if (options.grid.hoverable) { + eventHolder.mousemove(onMouseMove); + + // Use bind, rather than .mouseleave, because we officially + // still support jQuery 1.2.6, which doesn't define a shortcut + // for mouseenter or mouseleave. This was a bug/oversight that + // was fixed somewhere around 1.3.x. We can return to using + // .mouseleave when we drop support for 1.2.6. + + eventHolder.bind("mouseleave", onMouseLeave); + } + + if (options.grid.clickable) + eventHolder.click(onClick); + + executeHooks(hooks.bindEvents, [eventHolder]); + } + + function shutdown() { + if (redrawTimeout) + clearTimeout(redrawTimeout); + + eventHolder.unbind("mousemove", onMouseMove); + eventHolder.unbind("mouseleave", onMouseLeave); + eventHolder.unbind("click", onClick); + + executeHooks(hooks.shutdown, [eventHolder]); + } + + function setTransformationHelpers(axis) { + // set helper functions on the axis, assumes plot area + // has been computed already + + function identity(x) { return x; } + + var s, m, t = axis.options.transform || identity, + it = axis.options.inverseTransform; + + // precompute how much the axis is scaling a point + // in canvas space + if (axis.direction == "x") { + s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min)); + m = Math.min(t(axis.max), t(axis.min)); + } + else { + s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min)); + s = -s; + m = Math.max(t(axis.max), t(axis.min)); + } + + // data point to canvas coordinate + if (t == identity) // slight optimization + axis.p2c = function (p) { return (p - m) * s; }; + else + axis.p2c = function (p) { return (t(p) - m) * s; }; + // canvas coordinate to data point + if (!it) + axis.c2p = function (c) { return m + c / s; }; + else + axis.c2p = function (c) { return it(m + c / s); }; + } + + function measureTickLabels(axis) { + + var opts = axis.options, + ticks = axis.ticks || [], + labelWidth = opts.labelWidth || 0, + labelHeight = opts.labelHeight || 0, + maxWidth = labelWidth || (axis.direction == "x" ? Math.floor(surface.width / (ticks.length || 1)) : null), + legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", + layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, + font = opts.font || "flot-tick-label tickLabel"; + + for (var i = 0; i < ticks.length; ++i) { + + var t = ticks[i]; + + if (!t.label) + continue; + + var info = surface.getTextInfo(layer, t.label, font, null, maxWidth); + + labelWidth = Math.max(labelWidth, info.width); + labelHeight = Math.max(labelHeight, info.height); + } + + axis.labelWidth = opts.labelWidth || labelWidth; + axis.labelHeight = opts.labelHeight || labelHeight; + } + + function allocateAxisBoxFirstPhase(axis) { + // find the bounding box of the axis by looking at label + // widths/heights and ticks, make room by diminishing the + // plotOffset; this first phase only looks at one + // dimension per axis, the other dimension depends on the + // other axes so will have to wait + + var lw = axis.labelWidth, + lh = axis.labelHeight, + pos = axis.options.position, + isXAxis = axis.direction === "x", + tickLength = axis.options.tickLength, + axisMargin = options.grid.axisMargin, + padding = options.grid.labelMargin, + innermost = true, + outermost = true, + first = true, + found = false; + + // Determine the axis's position in its direction and on its side + + $.each(isXAxis ? xaxes : yaxes, function(i, a) { + if (a && (a.show || a.reserveSpace)) { + if (a === axis) { + found = true; + } else if (a.options.position === pos) { + if (found) { + outermost = false; + } else { + innermost = false; + } + } + if (!found) { + first = false; + } + } + }); + + // The outermost axis on each side has no margin + + if (outermost) { + axisMargin = 0; + } + + // The ticks for the first axis in each direction stretch across + + if (tickLength == null) { + tickLength = first ? "full" : 5; + } + + if (!isNaN(+tickLength)) + padding += +tickLength; + + if (isXAxis) { + lh += padding; + + if (pos == "bottom") { + plotOffset.bottom += lh + axisMargin; + axis.box = { top: surface.height - plotOffset.bottom, height: lh }; + } + else { + axis.box = { top: plotOffset.top + axisMargin, height: lh }; + plotOffset.top += lh + axisMargin; + } + } + else { + lw += padding; + + if (pos == "left") { + axis.box = { left: plotOffset.left + axisMargin, width: lw }; + plotOffset.left += lw + axisMargin; + } + else { + plotOffset.right += lw + axisMargin; + axis.box = { left: surface.width - plotOffset.right, width: lw }; + } + } + + // save for future reference + axis.position = pos; + axis.tickLength = tickLength; + axis.box.padding = padding; + axis.innermost = innermost; + } + + function allocateAxisBoxSecondPhase(axis) { + // now that all axis boxes have been placed in one + // dimension, we can set the remaining dimension coordinates + if (axis.direction == "x") { + axis.box.left = plotOffset.left - axis.labelWidth / 2; + axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.labelWidth; + } + else { + axis.box.top = plotOffset.top - axis.labelHeight / 2; + axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.labelHeight; + } + } + + function adjustLayoutForThingsStickingOut() { + // possibly adjust plot offset to ensure everything stays + // inside the canvas and isn't clipped off + + var minMargin = options.grid.minBorderMargin, + axis, i; + + // check stuff from the plot (FIXME: this should just read + // a value from the series, otherwise it's impossible to + // customize) + if (minMargin == null) { + minMargin = 0; + for (i = 0; i < series.length; ++i) + minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth/2)); + } + + var margins = { + left: minMargin, + right: minMargin, + top: minMargin, + bottom: minMargin + }; + + // check axis labels, note we don't check the actual + // labels but instead use the overall width/height to not + // jump as much around with replots + $.each(allAxes(), function (_, axis) { + if (axis.reserveSpace && axis.ticks && axis.ticks.length) { + if (axis.direction === "x") { + margins.left = Math.max(margins.left, axis.labelWidth / 2); + margins.right = Math.max(margins.right, axis.labelWidth / 2); + } else { + margins.bottom = Math.max(margins.bottom, axis.labelHeight / 2); + margins.top = Math.max(margins.top, axis.labelHeight / 2); + } + } + }); + + plotOffset.left = Math.ceil(Math.max(margins.left, plotOffset.left)); + plotOffset.right = Math.ceil(Math.max(margins.right, plotOffset.right)); + plotOffset.top = Math.ceil(Math.max(margins.top, plotOffset.top)); + plotOffset.bottom = Math.ceil(Math.max(margins.bottom, plotOffset.bottom)); + } + + function setupGrid() { + var i, axes = allAxes(), showGrid = options.grid.show; + + // Initialize the plot's offset from the edge of the canvas + + for (var a in plotOffset) { + var margin = options.grid.margin || 0; + plotOffset[a] = typeof margin == "number" ? margin : margin[a] || 0; + } + + executeHooks(hooks.processOffset, [plotOffset]); + + // If the grid is visible, add its border width to the offset + + for (var a in plotOffset) { + if(typeof(options.grid.borderWidth) == "object") { + plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0; + } + else { + plotOffset[a] += showGrid ? options.grid.borderWidth : 0; + } + } + + $.each(axes, function (_, axis) { + var axisOpts = axis.options; + axis.show = axisOpts.show == null ? axis.used : axisOpts.show; + axis.reserveSpace = axisOpts.reserveSpace == null ? axis.show : axisOpts.reserveSpace; + setRange(axis); + }); + + if (showGrid) { + + var allocatedAxes = $.grep(axes, function (axis) { + return axis.show || axis.reserveSpace; + }); + + $.each(allocatedAxes, function (_, axis) { + // make the ticks + setupTickGeneration(axis); + setTicks(axis); + snapRangeToTicks(axis, axis.ticks); + // find labelWidth/Height for axis + measureTickLabels(axis); + }); + + // with all dimensions calculated, we can compute the + // axis bounding boxes, start from the outside + // (reverse order) + for (i = allocatedAxes.length - 1; i >= 0; --i) + allocateAxisBoxFirstPhase(allocatedAxes[i]); + + // make sure we've got enough space for things that + // might stick out + adjustLayoutForThingsStickingOut(); + + $.each(allocatedAxes, function (_, axis) { + allocateAxisBoxSecondPhase(axis); + }); + } + + plotWidth = surface.width - plotOffset.left - plotOffset.right; + plotHeight = surface.height - plotOffset.bottom - plotOffset.top; + + // now we got the proper plot dimensions, we can compute the scaling + $.each(axes, function (_, axis) { + setTransformationHelpers(axis); + }); + + if (showGrid) { + drawAxisLabels(); + } + + insertLegend(); + } + + function setRange(axis) { + var opts = axis.options, + min = +(opts.min != null ? opts.min : axis.datamin), + max = +(opts.max != null ? opts.max : axis.datamax), + delta = max - min; + + if (delta == 0.0) { + // degenerate case + var widen = max == 0 ? 1 : 0.01; + + if (opts.min == null) + min -= widen; + // always widen max if we couldn't widen min to ensure we + // don't fall into min == max which doesn't work + if (opts.max == null || opts.min != null) + max += widen; + } + else { + // consider autoscaling + var margin = opts.autoscaleMargin; + if (margin != null) { + if (opts.min == null) { + min -= delta * margin; + // make sure we don't go below zero if all values + // are positive + if (min < 0 && axis.datamin != null && axis.datamin >= 0) + min = 0; + } + if (opts.max == null) { + max += delta * margin; + if (max > 0 && axis.datamax != null && axis.datamax <= 0) + max = 0; + } + } + } + axis.min = min; + axis.max = max; + } + + function setupTickGeneration(axis) { + var opts = axis.options; + + // estimate number of ticks + var noTicks; + if (typeof opts.ticks == "number" && opts.ticks > 0) + noTicks = opts.ticks; + else + // heuristic based on the model a*sqrt(x) fitted to + // some data points that seemed reasonable + noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? surface.width : surface.height); + + var delta = (axis.max - axis.min) / noTicks, + dec = -Math.floor(Math.log(delta) / Math.LN10), + maxDec = opts.tickDecimals; + + if (maxDec != null && dec > maxDec) { + dec = maxDec; + } + + var magn = Math.pow(10, -dec), + norm = delta / magn, // norm is between 1.0 and 10.0 + size; + + if (norm < 1.5) { + size = 1; + } else if (norm < 3) { + size = 2; + // special case for 2.5, requires an extra decimal + if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { + size = 2.5; + ++dec; + } + } else if (norm < 7.5) { + size = 5; + } else { + size = 10; + } + + size *= magn; + + if (opts.minTickSize != null && size < opts.minTickSize) { + size = opts.minTickSize; + } + + axis.delta = delta; + axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); + axis.tickSize = opts.tickSize || size; + + // Time mode was moved to a plug-in in 0.8, and since so many people use it + // we'll add an especially friendly reminder to make sure they included it. + + if (opts.mode == "time" && !axis.tickGenerator) { + throw new Error("Time mode requires the flot.time plugin."); + } + + // Flot supports base-10 axes; any other mode else is handled by a plug-in, + // like flot.time.js. + + if (!axis.tickGenerator) { + + axis.tickGenerator = function (axis) { + + var ticks = [], + start = floorInBase(axis.min, axis.tickSize), + i = 0, + v = Number.NaN, + prev; + + do { + prev = v; + v = start + i * axis.tickSize; + ticks.push(v); + ++i; + } while (v < axis.max && v != prev); + return ticks; + }; + + axis.tickFormatter = function (value, axis) { + + var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1; + var formatted = "" + Math.round(value * factor) / factor; + + // If tickDecimals was specified, ensure that we have exactly that + // much precision; otherwise default to the value's own precision. + + if (axis.tickDecimals != null) { + var decimal = formatted.indexOf("."); + var precision = decimal == -1 ? 0 : formatted.length - decimal - 1; + if (precision < axis.tickDecimals) { + return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision); + } + } + + return formatted; + }; + } + + if ($.isFunction(opts.tickFormatter)) + axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); }; + + if (opts.alignTicksWithAxis != null) { + var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1]; + if (otherAxis && otherAxis.used && otherAxis != axis) { + // consider snapping min/max to outermost nice ticks + var niceTicks = axis.tickGenerator(axis); + if (niceTicks.length > 0) { + if (opts.min == null) + axis.min = Math.min(axis.min, niceTicks[0]); + if (opts.max == null && niceTicks.length > 1) + axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]); + } + + axis.tickGenerator = function (axis) { + // copy ticks, scaled to this axis + var ticks = [], v, i; + for (i = 0; i < otherAxis.ticks.length; ++i) { + v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min); + v = axis.min + v * (axis.max - axis.min); + ticks.push(v); + } + return ticks; + }; + + // we might need an extra decimal since forced + // ticks don't necessarily fit naturally + if (!axis.mode && opts.tickDecimals == null) { + var extraDec = Math.max(0, -Math.floor(Math.log(axis.delta) / Math.LN10) + 1), + ts = axis.tickGenerator(axis); + + // only proceed if the tick interval rounded + // with an extra decimal doesn't give us a + // zero at end + if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec)))) + axis.tickDecimals = extraDec; + } + } + } + } + + function setTicks(axis) { + var oticks = axis.options.ticks, ticks = []; + if (oticks == null || (typeof oticks == "number" && oticks > 0)) + ticks = axis.tickGenerator(axis); + else if (oticks) { + if ($.isFunction(oticks)) + // generate the ticks + ticks = oticks(axis); + else + ticks = oticks; + } + + // clean up/labelify the supplied ticks, copy them over + var i, v; + axis.ticks = []; + for (i = 0; i < ticks.length; ++i) { + var label = null; + var t = ticks[i]; + if (typeof t == "object") { + v = +t[0]; + if (t.length > 1) + label = t[1]; + } + else + v = +t; + if (label == null) + label = axis.tickFormatter(v, axis); + if (!isNaN(v)) + axis.ticks.push({ v: v, label: label }); + } + } + + function snapRangeToTicks(axis, ticks) { + if (axis.options.autoscaleMargin && ticks.length > 0) { + // snap to ticks + if (axis.options.min == null) + axis.min = Math.min(axis.min, ticks[0].v); + if (axis.options.max == null && ticks.length > 1) + axis.max = Math.max(axis.max, ticks[ticks.length - 1].v); + } + } + + function draw() { + + surface.clear(); + + executeHooks(hooks.drawBackground, [ctx]); + + var grid = options.grid; + + // draw background, if any + if (grid.show && grid.backgroundColor) + drawBackground(); + + if (grid.show && !grid.aboveData) { + drawGrid(); + } + + for (var i = 0; i < series.length; ++i) { + executeHooks(hooks.drawSeries, [ctx, series[i]]); + drawSeries(series[i]); + } + + executeHooks(hooks.draw, [ctx]); + + if (grid.show && grid.aboveData) { + drawGrid(); + } + + surface.render(); + + // A draw implies that either the axes or data have changed, so we + // should probably update the overlay highlights as well. + + triggerRedrawOverlay(); + } + + function extractRange(ranges, coord) { + var axis, from, to, key, axes = allAxes(); + + for (var i = 0; i < axes.length; ++i) { + axis = axes[i]; + if (axis.direction == coord) { + key = coord + axis.n + "axis"; + if (!ranges[key] && axis.n == 1) + key = coord + "axis"; // support x1axis as xaxis + if (ranges[key]) { + from = ranges[key].from; + to = ranges[key].to; + break; + } + } + } + + // backwards-compat stuff - to be removed in future + if (!ranges[key]) { + axis = coord == "x" ? xaxes[0] : yaxes[0]; + from = ranges[coord + "1"]; + to = ranges[coord + "2"]; + } + + // auto-reverse as an added bonus + if (from != null && to != null && from > to) { + var tmp = from; + from = to; + to = tmp; + } + + return { from: from, to: to, axis: axis }; + } + + function drawBackground() { + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); + ctx.fillRect(0, 0, plotWidth, plotHeight); + ctx.restore(); + } + + function drawGrid() { + var i, axes, bw, bc; + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + // draw markings + var markings = options.grid.markings; + if (markings) { + if ($.isFunction(markings)) { + axes = plot.getAxes(); + // xmin etc. is backwards compatibility, to be + // removed in the future + axes.xmin = axes.xaxis.min; + axes.xmax = axes.xaxis.max; + axes.ymin = axes.yaxis.min; + axes.ymax = axes.yaxis.max; + + markings = markings(axes); + } + + for (i = 0; i < markings.length; ++i) { + var m = markings[i], + xrange = extractRange(m, "x"), + yrange = extractRange(m, "y"); + + // fill in missing + if (xrange.from == null) + xrange.from = xrange.axis.min; + if (xrange.to == null) + xrange.to = xrange.axis.max; + if (yrange.from == null) + yrange.from = yrange.axis.min; + if (yrange.to == null) + yrange.to = yrange.axis.max; + + // clip + if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max || + yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) + continue; + + xrange.from = Math.max(xrange.from, xrange.axis.min); + xrange.to = Math.min(xrange.to, xrange.axis.max); + yrange.from = Math.max(yrange.from, yrange.axis.min); + yrange.to = Math.min(yrange.to, yrange.axis.max); + + var xequal = xrange.from === xrange.to, + yequal = yrange.from === yrange.to; + + if (xequal && yequal) { + continue; + } + + // then draw + xrange.from = Math.floor(xrange.axis.p2c(xrange.from)); + xrange.to = Math.floor(xrange.axis.p2c(xrange.to)); + yrange.from = Math.floor(yrange.axis.p2c(yrange.from)); + yrange.to = Math.floor(yrange.axis.p2c(yrange.to)); + + if (xequal || yequal) { + var lineWidth = m.lineWidth || options.grid.markingsLineWidth, + subPixel = lineWidth % 2 ? 0.5 : 0; + ctx.beginPath(); + ctx.strokeStyle = m.color || options.grid.markingsColor; + ctx.lineWidth = lineWidth; + if (xequal) { + ctx.moveTo(xrange.to + subPixel, yrange.from); + ctx.lineTo(xrange.to + subPixel, yrange.to); + } else { + ctx.moveTo(xrange.from, yrange.to + subPixel); + ctx.lineTo(xrange.to, yrange.to + subPixel); + } + ctx.stroke(); + } else { + ctx.fillStyle = m.color || options.grid.markingsColor; + ctx.fillRect(xrange.from, yrange.to, + xrange.to - xrange.from, + yrange.from - yrange.to); + } + } + } + + // draw the ticks + axes = allAxes(); + bw = options.grid.borderWidth; + + for (var j = 0; j < axes.length; ++j) { + var axis = axes[j], box = axis.box, + t = axis.tickLength, x, y, xoff, yoff; + if (!axis.show || axis.ticks.length == 0) + continue; + + ctx.lineWidth = 1; + + // find the edges + if (axis.direction == "x") { + x = 0; + if (t == "full") + y = (axis.position == "top" ? 0 : plotHeight); + else + y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0); + } + else { + y = 0; + if (t == "full") + x = (axis.position == "left" ? 0 : plotWidth); + else + x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0); + } + + // draw tick bar + if (!axis.innermost) { + ctx.strokeStyle = axis.options.color; + ctx.beginPath(); + xoff = yoff = 0; + if (axis.direction == "x") + xoff = plotWidth + 1; + else + yoff = plotHeight + 1; + + if (ctx.lineWidth == 1) { + if (axis.direction == "x") { + y = Math.floor(y) + 0.5; + } else { + x = Math.floor(x) + 0.5; + } + } + + ctx.moveTo(x, y); + ctx.lineTo(x + xoff, y + yoff); + ctx.stroke(); + } + + // draw ticks + + ctx.strokeStyle = axis.options.tickColor; + + ctx.beginPath(); + for (i = 0; i < axis.ticks.length; ++i) { + var v = axis.ticks[i].v; + + xoff = yoff = 0; + + if (isNaN(v) || v < axis.min || v > axis.max + // skip those lying on the axes if we got a border + || (t == "full" + && ((typeof bw == "object" && bw[axis.position] > 0) || bw > 0) + && (v == axis.min || v == axis.max))) + continue; + + if (axis.direction == "x") { + x = axis.p2c(v); + yoff = t == "full" ? -plotHeight : t; + + if (axis.position == "top") + yoff = -yoff; + } + else { + y = axis.p2c(v); + xoff = t == "full" ? -plotWidth : t; + + if (axis.position == "left") + xoff = -xoff; + } + + if (ctx.lineWidth == 1) { + if (axis.direction == "x") + x = Math.floor(x) + 0.5; + else + y = Math.floor(y) + 0.5; + } + + ctx.moveTo(x, y); + ctx.lineTo(x + xoff, y + yoff); + } + + ctx.stroke(); + } + + + // draw border + if (bw) { + // If either borderWidth or borderColor is an object, then draw the border + // line by line instead of as one rectangle + bc = options.grid.borderColor; + if(typeof bw == "object" || typeof bc == "object") { + if (typeof bw !== "object") { + bw = {top: bw, right: bw, bottom: bw, left: bw}; + } + if (typeof bc !== "object") { + bc = {top: bc, right: bc, bottom: bc, left: bc}; + } + + if (bw.top > 0) { + ctx.strokeStyle = bc.top; + ctx.lineWidth = bw.top; + ctx.beginPath(); + ctx.moveTo(0 - bw.left, 0 - bw.top/2); + ctx.lineTo(plotWidth, 0 - bw.top/2); + ctx.stroke(); + } + + if (bw.right > 0) { + ctx.strokeStyle = bc.right; + ctx.lineWidth = bw.right; + ctx.beginPath(); + ctx.moveTo(plotWidth + bw.right / 2, 0 - bw.top); + ctx.lineTo(plotWidth + bw.right / 2, plotHeight); + ctx.stroke(); + } + + if (bw.bottom > 0) { + ctx.strokeStyle = bc.bottom; + ctx.lineWidth = bw.bottom; + ctx.beginPath(); + ctx.moveTo(plotWidth + bw.right, plotHeight + bw.bottom / 2); + ctx.lineTo(0, plotHeight + bw.bottom / 2); + ctx.stroke(); + } + + if (bw.left > 0) { + ctx.strokeStyle = bc.left; + ctx.lineWidth = bw.left; + ctx.beginPath(); + ctx.moveTo(0 - bw.left/2, plotHeight + bw.bottom); + ctx.lineTo(0- bw.left/2, 0); + ctx.stroke(); + } + } + else { + ctx.lineWidth = bw; + ctx.strokeStyle = options.grid.borderColor; + ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw); + } + } + + ctx.restore(); + } + + function drawAxisLabels() { + + $.each(allAxes(), function (_, axis) { + var box = axis.box, + legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", + layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, + font = axis.options.font || "flot-tick-label tickLabel", + tick, x, y, halign, valign; + + // Remove text before checking for axis.show and ticks.length; + // otherwise plugins, like flot-tickrotor, that draw their own + // tick labels will end up with both theirs and the defaults. + + surface.removeText(layer); + + if (!axis.show || axis.ticks.length == 0) + return; + + for (var i = 0; i < axis.ticks.length; ++i) { + + tick = axis.ticks[i]; + if (!tick.label || tick.v < axis.min || tick.v > axis.max) + continue; + + if (axis.direction == "x") { + halign = "center"; + x = plotOffset.left + axis.p2c(tick.v); + if (axis.position == "bottom") { + y = box.top + box.padding; + } else { + y = box.top + box.height - box.padding; + valign = "bottom"; + } + } else { + valign = "middle"; + y = plotOffset.top + axis.p2c(tick.v); + if (axis.position == "left") { + x = box.left + box.width - box.padding; + halign = "right"; + } else { + x = box.left + box.padding; + } + } + + surface.addText(layer, x, y, tick.label, font, null, null, halign, valign); + } + }); + } + + function drawSeries(series) { + if (series.lines.show) + drawSeriesLines(series); + if (series.bars.show) + drawSeriesBars(series); + if (series.points.show) + drawSeriesPoints(series); + } + + function drawSeriesLines(series) { + function plotLine(datapoints, xoffset, yoffset, axisx, axisy) { + var points = datapoints.points, + ps = datapoints.pointsize, + prevx = null, prevy = null; + + ctx.beginPath(); + for (var i = ps; i < points.length; i += ps) { + var x1 = points[i - ps], y1 = points[i - ps + 1], + x2 = points[i], y2 = points[i + 1]; + + if (x1 == null || x2 == null) + continue; + + // clip with ymin + if (y1 <= y2 && y1 < axisy.min) { + if (y2 < axisy.min) + continue; // line segment is outside + // compute new intersection point + x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.min; + } + else if (y2 <= y1 && y2 < axisy.min) { + if (y1 < axisy.min) + continue; + x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.min; + } + + // clip with ymax + if (y1 >= y2 && y1 > axisy.max) { + if (y2 > axisy.max) + continue; + x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.max; + } + else if (y2 >= y1 && y2 > axisy.max) { + if (y1 > axisy.max) + continue; + x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.max; + } + + // clip with xmin + if (x1 <= x2 && x1 < axisx.min) { + if (x2 < axisx.min) + continue; + y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.min; + } + else if (x2 <= x1 && x2 < axisx.min) { + if (x1 < axisx.min) + continue; + y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.min; + } + + // clip with xmax + if (x1 >= x2 && x1 > axisx.max) { + if (x2 > axisx.max) + continue; + y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.max; + } + else if (x2 >= x1 && x2 > axisx.max) { + if (x1 > axisx.max) + continue; + y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.max; + } + + if (x1 != prevx || y1 != prevy) + ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); + + prevx = x2; + prevy = y2; + ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset); + } + ctx.stroke(); + } + + function plotLineArea(datapoints, axisx, axisy) { + var points = datapoints.points, + ps = datapoints.pointsize, + bottom = Math.min(Math.max(0, axisy.min), axisy.max), + i = 0, top, areaOpen = false, + ypos = 1, segmentStart = 0, segmentEnd = 0; + + // we process each segment in two turns, first forward + // direction to sketch out top, then once we hit the + // end we go backwards to sketch the bottom + while (true) { + if (ps > 0 && i > points.length + ps) + break; + + i += ps; // ps is negative if going backwards + + var x1 = points[i - ps], + y1 = points[i - ps + ypos], + x2 = points[i], y2 = points[i + ypos]; + + if (areaOpen) { + if (ps > 0 && x1 != null && x2 == null) { + // at turning point + segmentEnd = i; + ps = -ps; + ypos = 2; + continue; + } + + if (ps < 0 && i == segmentStart + ps) { + // done with the reverse sweep + ctx.fill(); + areaOpen = false; + ps = -ps; + ypos = 1; + i = segmentStart = segmentEnd + ps; + continue; + } + } + + if (x1 == null || x2 == null) + continue; + + // clip x values + + // clip with xmin + if (x1 <= x2 && x1 < axisx.min) { + if (x2 < axisx.min) + continue; + y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.min; + } + else if (x2 <= x1 && x2 < axisx.min) { + if (x1 < axisx.min) + continue; + y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.min; + } + + // clip with xmax + if (x1 >= x2 && x1 > axisx.max) { + if (x2 > axisx.max) + continue; + y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.max; + } + else if (x2 >= x1 && x2 > axisx.max) { + if (x1 > axisx.max) + continue; + y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.max; + } + + if (!areaOpen) { + // open area + ctx.beginPath(); + ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom)); + areaOpen = true; + } + + // now first check the case where both is outside + if (y1 >= axisy.max && y2 >= axisy.max) { + ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); + continue; + } + else if (y1 <= axisy.min && y2 <= axisy.min) { + ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); + continue; + } + + // else it's a bit more complicated, there might + // be a flat maxed out rectangle first, then a + // triangular cutout or reverse; to find these + // keep track of the current x values + var x1old = x1, x2old = x2; + + // clip the y values, without shortcutting, we + // go through all cases in turn + + // clip with ymin + if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { + x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.min; + } + else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { + x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.min; + } + + // clip with ymax + if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { + x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.max; + } + else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { + x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.max; + } + + // if the x value was changed we got a rectangle + // to fill + if (x1 != x1old) { + ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1)); + // it goes to (x1, y1), but we fill that below + } + + // fill triangular section, this sometimes result + // in redundant points if (x1, y1) hasn't changed + // from previous line to, but we just ignore that + ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); + + // fill the other rectangle if it's there + if (x2 != x2old) { + ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); + ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2)); + } + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + ctx.lineJoin = "round"; + + var lw = series.lines.lineWidth, + sw = series.shadowSize; + // FIXME: consider another form of shadow when filling is turned on + if (lw > 0 && sw > 0) { + // draw shadow as a thick and thin line with transparency + ctx.lineWidth = sw; + ctx.strokeStyle = "rgba(0,0,0,0.1)"; + // position shadow at angle from the mid of line + var angle = Math.PI/18; + plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis); + ctx.lineWidth = sw/2; + plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis); + } + + ctx.lineWidth = lw; + ctx.strokeStyle = series.color; + var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight); + if (fillStyle) { + ctx.fillStyle = fillStyle; + plotLineArea(series.datapoints, series.xaxis, series.yaxis); + } + + if (lw > 0) + plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis); + ctx.restore(); + } + + function drawSeriesPoints(series) { + function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) { + var points = datapoints.points, ps = datapoints.pointsize; + + for (var i = 0; i < points.length; i += ps) { + var x = points[i], y = points[i + 1]; + if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) + continue; + + ctx.beginPath(); + x = axisx.p2c(x); + y = axisy.p2c(y) + offset; + if (symbol == "circle") + ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false); + else + symbol(ctx, x, y, radius, shadow); + ctx.closePath(); + + if (fillStyle) { + ctx.fillStyle = fillStyle; + ctx.fill(); + } + ctx.stroke(); + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + var lw = series.points.lineWidth, + sw = series.shadowSize, + radius = series.points.radius, + symbol = series.points.symbol; + + // If the user sets the line width to 0, we change it to a very + // small value. A line width of 0 seems to force the default of 1. + // Doing the conditional here allows the shadow setting to still be + // optional even with a lineWidth of 0. + + if( lw == 0 ) + lw = 0.0001; + + if (lw > 0 && sw > 0) { + // draw shadow in two steps + var w = sw / 2; + ctx.lineWidth = w; + ctx.strokeStyle = "rgba(0,0,0,0.1)"; + plotPoints(series.datapoints, radius, null, w + w/2, true, + series.xaxis, series.yaxis, symbol); + + ctx.strokeStyle = "rgba(0,0,0,0.2)"; + plotPoints(series.datapoints, radius, null, w/2, true, + series.xaxis, series.yaxis, symbol); + } + + ctx.lineWidth = lw; + ctx.strokeStyle = series.color; + plotPoints(series.datapoints, radius, + getFillStyle(series.points, series.color), 0, false, + series.xaxis, series.yaxis, symbol); + ctx.restore(); + } + + function drawBar(x, y, b, barLeft, barRight, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) { + var left, right, bottom, top, + drawLeft, drawRight, drawTop, drawBottom, + tmp; + + // in horizontal mode, we start the bar from the left + // instead of from the bottom so it appears to be + // horizontal rather than vertical + if (horizontal) { + drawBottom = drawRight = drawTop = true; + drawLeft = false; + left = b; + right = x; + top = y + barLeft; + bottom = y + barRight; + + // account for negative bars + if (right < left) { + tmp = right; + right = left; + left = tmp; + drawLeft = true; + drawRight = false; + } + } + else { + drawLeft = drawRight = drawTop = true; + drawBottom = false; + left = x + barLeft; + right = x + barRight; + bottom = b; + top = y; + + // account for negative bars + if (top < bottom) { + tmp = top; + top = bottom; + bottom = tmp; + drawBottom = true; + drawTop = false; + } + } + + // clip + if (right < axisx.min || left > axisx.max || + top < axisy.min || bottom > axisy.max) + return; + + if (left < axisx.min) { + left = axisx.min; + drawLeft = false; + } + + if (right > axisx.max) { + right = axisx.max; + drawRight = false; + } + + if (bottom < axisy.min) { + bottom = axisy.min; + drawBottom = false; + } + + if (top > axisy.max) { + top = axisy.max; + drawTop = false; + } + + left = axisx.p2c(left); + bottom = axisy.p2c(bottom); + right = axisx.p2c(right); + top = axisy.p2c(top); + + // fill the bar + if (fillStyleCallback) { + c.fillStyle = fillStyleCallback(bottom, top); + c.fillRect(left, top, right - left, bottom - top) + } + + // draw outline + if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) { + c.beginPath(); + + // FIXME: inline moveTo is buggy with excanvas + c.moveTo(left, bottom); + if (drawLeft) + c.lineTo(left, top); + else + c.moveTo(left, top); + if (drawTop) + c.lineTo(right, top); + else + c.moveTo(right, top); + if (drawRight) + c.lineTo(right, bottom); + else + c.moveTo(right, bottom); + if (drawBottom) + c.lineTo(left, bottom); + else + c.moveTo(left, bottom); + c.stroke(); + } + } + + function drawSeriesBars(series) { + function plotBars(datapoints, barLeft, barRight, fillStyleCallback, axisx, axisy) { + var points = datapoints.points, ps = datapoints.pointsize; + + for (var i = 0; i < points.length; i += ps) { + if (points[i] == null) + continue; + drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth); + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + // FIXME: figure out a way to add shadows (for instance along the right edge) + ctx.lineWidth = series.bars.lineWidth; + ctx.strokeStyle = series.color; + + var barLeft; + + switch (series.bars.align) { + case "left": + barLeft = 0; + break; + case "right": + barLeft = -series.bars.barWidth; + break; + default: + barLeft = -series.bars.barWidth / 2; + } + + var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null; + plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, fillStyleCallback, series.xaxis, series.yaxis); + ctx.restore(); + } + + function getFillStyle(filloptions, seriesColor, bottom, top) { + var fill = filloptions.fill; + if (!fill) + return null; + + if (filloptions.fillColor) + return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor); + + var c = $.color.parse(seriesColor); + c.a = typeof fill == "number" ? fill : 0.4; + c.normalize(); + return c.toString(); + } + + function insertLegend() { + + if (options.legend.container != null) { + $(options.legend.container).html(""); + } else { + placeholder.find(".legend").remove(); + } + + if (!options.legend.show) { + return; + } + + var fragments = [], entries = [], rowStarted = false, + lf = options.legend.labelFormatter, s, label; + + // Build a list of legend entries, with each having a label and a color + + for (var i = 0; i < series.length; ++i) { + s = series[i]; + if (s.label) { + label = lf ? lf(s.label, s) : s.label; + if (label) { + entries.push({ + label: label, + color: s.color + }); + } + } + } + + // Sort the legend using either the default or a custom comparator + + if (options.legend.sorted) { + if ($.isFunction(options.legend.sorted)) { + entries.sort(options.legend.sorted); + } else if (options.legend.sorted == "reverse") { + entries.reverse(); + } else { + var ascending = options.legend.sorted != "descending"; + entries.sort(function(a, b) { + return a.label == b.label ? 0 : ( + (a.label < b.label) != ascending ? 1 : -1 // Logical XOR + ); + }); + } + } + + // Generate markup for the list of entries, in their final order + + for (var i = 0; i < entries.length; ++i) { + + var entry = entries[i]; + + if (i % options.legend.noColumns == 0) { + if (rowStarted) + fragments.push(''); + fragments.push(''); + rowStarted = true; + } + + fragments.push( + '
' + + '' + entry.label + '' + ); + } + + if (rowStarted) + fragments.push(''); + + if (fragments.length == 0) + return; + + var table = '' + fragments.join("") + '
'; + if (options.legend.container != null) + $(options.legend.container).html(table); + else { + var pos = "", + p = options.legend.position, + m = options.legend.margin; + if (m[0] == null) + m = [m, m]; + if (p.charAt(0) == "n") + pos += 'top:' + (m[1] + plotOffset.top) + 'px;'; + else if (p.charAt(0) == "s") + pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;'; + if (p.charAt(1) == "e") + pos += 'right:' + (m[0] + plotOffset.right) + 'px;'; + else if (p.charAt(1) == "w") + pos += 'left:' + (m[0] + plotOffset.left) + 'px;'; + var legend = $('
' + table.replace('style="', 'style="position:absolute;' + pos +';') + '
').appendTo(placeholder); + if (options.legend.backgroundOpacity != 0.0) { + // put in the transparent background + // separately to avoid blended labels and + // label boxes + var c = options.legend.backgroundColor; + if (c == null) { + c = options.grid.backgroundColor; + if (c && typeof c == "string") + c = $.color.parse(c); + else + c = $.color.extract(legend, 'background-color'); + c.a = 1; + c = c.toString(); + } + var div = legend.children(); + $('
').prependTo(legend).css('opacity', options.legend.backgroundOpacity); + } + } + } + + + // interactive features + + var highlights = [], + redrawTimeout = null; + + // returns the data item the mouse is over, or null if none is found + function findNearbyItem(mouseX, mouseY, seriesFilter) { + var maxDistance = options.grid.mouseActiveRadius, + smallestDistance = maxDistance * maxDistance + 1, + item = null, foundPoint = false, i, j, ps; + + for (i = series.length - 1; i >= 0; --i) { + if (!seriesFilter(series[i])) + continue; + + var s = series[i], + axisx = s.xaxis, + axisy = s.yaxis, + points = s.datapoints.points, + mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster + my = axisy.c2p(mouseY), + maxx = maxDistance / axisx.scale, + maxy = maxDistance / axisy.scale; + + ps = s.datapoints.pointsize; + // with inverse transforms, we can't use the maxx/maxy + // optimization, sadly + if (axisx.options.inverseTransform) + maxx = Number.MAX_VALUE; + if (axisy.options.inverseTransform) + maxy = Number.MAX_VALUE; + + if (s.lines.show || s.points.show) { + for (j = 0; j < points.length; j += ps) { + var x = points[j], y = points[j + 1]; + if (x == null) + continue; + + // For points and lines, the cursor must be within a + // certain distance to the data point + if (x - mx > maxx || x - mx < -maxx || + y - my > maxy || y - my < -maxy) + continue; + + // We have to calculate distances in pixels, not in + // data units, because the scales of the axes may be different + var dx = Math.abs(axisx.p2c(x) - mouseX), + dy = Math.abs(axisy.p2c(y) - mouseY), + dist = dx * dx + dy * dy; // we save the sqrt + + // use <= to ensure last point takes precedence + // (last generally means on top of) + if (dist < smallestDistance) { + smallestDistance = dist; + item = [i, j / ps]; + } + } + } + + if (s.bars.show && !item) { // no other point can be nearby + + var barLeft, barRight; + + switch (s.bars.align) { + case "left": + barLeft = 0; + break; + case "right": + barLeft = -s.bars.barWidth; + break; + default: + barLeft = -s.bars.barWidth / 2; + } + + barRight = barLeft + s.bars.barWidth; + + for (j = 0; j < points.length; j += ps) { + var x = points[j], y = points[j + 1], b = points[j + 2]; + if (x == null) + continue; + + // for a bar graph, the cursor must be inside the bar + if (series[i].bars.horizontal ? + (mx <= Math.max(b, x) && mx >= Math.min(b, x) && + my >= y + barLeft && my <= y + barRight) : + (mx >= x + barLeft && mx <= x + barRight && + my >= Math.min(b, y) && my <= Math.max(b, y))) + item = [i, j / ps]; + } + } + } + + if (item) { + i = item[0]; + j = item[1]; + ps = series[i].datapoints.pointsize; + + return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps), + dataIndex: j, + series: series[i], + seriesIndex: i }; + } + + return null; + } + + function onMouseMove(e) { + if (options.grid.hoverable) + triggerClickHoverEvent("plothover", e, + function (s) { return s["hoverable"] != false; }); + } + + function onMouseLeave(e) { + if (options.grid.hoverable) + triggerClickHoverEvent("plothover", e, + function (s) { return false; }); + } + + function onClick(e) { + triggerClickHoverEvent("plotclick", e, + function (s) { return s["clickable"] != false; }); + } + + // trigger click or hover event (they send the same parameters + // so we share their code) + function triggerClickHoverEvent(eventname, event, seriesFilter) { + var offset = eventHolder.offset(), + canvasX = event.pageX - offset.left - plotOffset.left, + canvasY = event.pageY - offset.top - plotOffset.top, + pos = canvasToAxisCoords({ left: canvasX, top: canvasY }); + + pos.pageX = event.pageX; + pos.pageY = event.pageY; + + var item = findNearbyItem(canvasX, canvasY, seriesFilter); + + if (item) { + // fill in mouse pos for any listeners out there + item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left, 10); + item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top, 10); + } + + if (options.grid.autoHighlight) { + // clear auto-highlights + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.auto == eventname && + !(item && h.series == item.series && + h.point[0] == item.datapoint[0] && + h.point[1] == item.datapoint[1])) + unhighlight(h.series, h.point); + } + + if (item) + highlight(item.series, item.datapoint, eventname); + } + + placeholder.trigger(eventname, [ pos, item ]); + } + + function triggerRedrawOverlay() { + var t = options.interaction.redrawOverlayInterval; + if (t == -1) { // skip event queue + drawOverlay(); + return; + } + + if (!redrawTimeout) + redrawTimeout = setTimeout(drawOverlay, t); + } + + function drawOverlay() { + redrawTimeout = null; + + // draw highlights + octx.save(); + overlay.clear(); + octx.translate(plotOffset.left, plotOffset.top); + + var i, hi; + for (i = 0; i < highlights.length; ++i) { + hi = highlights[i]; + + if (hi.series.bars.show) + drawBarHighlight(hi.series, hi.point); + else + drawPointHighlight(hi.series, hi.point); + } + octx.restore(); + + executeHooks(hooks.drawOverlay, [octx]); + } + + function highlight(s, point, auto) { + if (typeof s == "number") + s = series[s]; + + if (typeof point == "number") { + var ps = s.datapoints.pointsize; + point = s.datapoints.points.slice(ps * point, ps * (point + 1)); + } + + var i = indexOfHighlight(s, point); + if (i == -1) { + highlights.push({ series: s, point: point, auto: auto }); + + triggerRedrawOverlay(); + } + else if (!auto) + highlights[i].auto = false; + } + + function unhighlight(s, point) { + if (s == null && point == null) { + highlights = []; + triggerRedrawOverlay(); + return; + } + + if (typeof s == "number") + s = series[s]; + + if (typeof point == "number") { + var ps = s.datapoints.pointsize; + point = s.datapoints.points.slice(ps * point, ps * (point + 1)); + } + + var i = indexOfHighlight(s, point); + if (i != -1) { + highlights.splice(i, 1); + + triggerRedrawOverlay(); + } + } + + function indexOfHighlight(s, p) { + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.series == s && h.point[0] == p[0] + && h.point[1] == p[1]) + return i; + } + return -1; + } + + function drawPointHighlight(series, point) { + var x = point[0], y = point[1], + axisx = series.xaxis, axisy = series.yaxis, + highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(); + + if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) + return; + + var pointRadius = series.points.radius + series.points.lineWidth / 2; + octx.lineWidth = pointRadius; + octx.strokeStyle = highlightColor; + var radius = 1.5 * pointRadius; + x = axisx.p2c(x); + y = axisy.p2c(y); + + octx.beginPath(); + if (series.points.symbol == "circle") + octx.arc(x, y, radius, 0, 2 * Math.PI, false); + else + series.points.symbol(octx, x, y, radius, false); + octx.closePath(); + octx.stroke(); + } + + function drawBarHighlight(series, point) { + var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(), + fillStyle = highlightColor, + barLeft; + + switch (series.bars.align) { + case "left": + barLeft = 0; + break; + case "right": + barLeft = -series.bars.barWidth; + break; + default: + barLeft = -series.bars.barWidth / 2; + } + + octx.lineWidth = series.bars.lineWidth; + octx.strokeStyle = highlightColor; + + drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth, + function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth); + } + + function getColorOrGradient(spec, bottom, top, defaultColor) { + if (typeof spec == "string") + return spec; + else { + // assume this is a gradient spec; IE currently only + // supports a simple vertical gradient properly, so that's + // what we support too + var gradient = ctx.createLinearGradient(0, top, 0, bottom); + + for (var i = 0, l = spec.colors.length; i < l; ++i) { + var c = spec.colors[i]; + if (typeof c != "string") { + var co = $.color.parse(defaultColor); + if (c.brightness != null) + co = co.scale('rgb', c.brightness); + if (c.opacity != null) + co.a *= c.opacity; + c = co.toString(); + } + gradient.addColorStop(i / (l - 1), c); + } + + return gradient; + } + } + } + + // Add the plot function to the top level of the jQuery object + + $.plot = function(placeholder, data, options) { + //var t0 = new Date(); + var plot = new Plot($(placeholder), data, options, $.plot.plugins); + //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime())); + return plot; + }; + + $.plot.version = "0.8.3"; + + $.plot.plugins = []; + + // Also add the plot function as a chainable property + + $.fn.plot = function(data, options) { + return this.each(function() { + $.plot(this, data, options); + }); + }; + + // round to nearby lower multiple of base + function floorInBase(n, base) { + return base * Math.floor(n / base); + } + +})(jQuery); diff --git a/src/plugins/timelion/public/webpackShims/jquery.flot.selection.js b/src/plugins/timelion/public/webpackShims/jquery.flot.selection.js new file mode 100644 index 0000000000000..c8707b30f4e6f --- /dev/null +++ b/src/plugins/timelion/public/webpackShims/jquery.flot.selection.js @@ -0,0 +1,360 @@ +/* Flot plugin for selecting regions of a plot. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The plugin supports these options: + +selection: { + mode: null or "x" or "y" or "xy", + color: color, + shape: "round" or "miter" or "bevel", + minSize: number of pixels +} + +Selection support is enabled by setting the mode to one of "x", "y" or "xy". +In "x" mode, the user will only be able to specify the x range, similarly for +"y" mode. For "xy", the selection becomes a rectangle where both ranges can be +specified. "color" is color of the selection (if you need to change the color +later on, you can get to it with plot.getOptions().selection.color). "shape" +is the shape of the corners of the selection. + +"minSize" is the minimum size a selection can be in pixels. This value can +be customized to determine the smallest size a selection can be and still +have the selection rectangle be displayed. When customizing this value, the +fact that it refers to pixels, not axis units must be taken into account. +Thus, for example, if there is a bar graph in time mode with BarWidth set to 1 +minute, setting "minSize" to 1 will not make the minimum selection size 1 +minute, but rather 1 pixel. Note also that setting "minSize" to 0 will prevent +"plotunselected" events from being fired when the user clicks the mouse without +dragging. + +When selection support is enabled, a "plotselected" event will be emitted on +the DOM element you passed into the plot function. The event handler gets a +parameter with the ranges selected on the axes, like this: + + placeholder.bind( "plotselected", function( event, ranges ) { + alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to) + // similar for yaxis - with multiple axes, the extra ones are in + // x2axis, x3axis, ... + }); + +The "plotselected" event is only fired when the user has finished making the +selection. A "plotselecting" event is fired during the process with the same +parameters as the "plotselected" event, in case you want to know what's +happening while it's happening, + +A "plotunselected" event with no arguments is emitted when the user clicks the +mouse to remove the selection. As stated above, setting "minSize" to 0 will +destroy this behavior. + +The plugin also adds the following methods to the plot object: + +- setSelection( ranges, preventEvent ) + + Set the selection rectangle. The passed in ranges is on the same form as + returned in the "plotselected" event. If the selection mode is "x", you + should put in either an xaxis range, if the mode is "y" you need to put in + an yaxis range and both xaxis and yaxis if the selection mode is "xy", like + this: + + setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } }); + + setSelection will trigger the "plotselected" event when called. If you don't + want that to happen, e.g. if you're inside a "plotselected" handler, pass + true as the second parameter. If you are using multiple axes, you can + specify the ranges on any of those, e.g. as x2axis/x3axis/... instead of + xaxis, the plugin picks the first one it sees. + +- clearSelection( preventEvent ) + + Clear the selection rectangle. Pass in true to avoid getting a + "plotunselected" event. + +- getSelection() + + Returns the current selection in the same format as the "plotselected" + event. If there's currently no selection, the function returns null. + +*/ + +(function ($) { + function init(plot) { + var selection = { + first: { x: -1, y: -1}, second: { x: -1, y: -1}, + show: false, + active: false + }; + + // FIXME: The drag handling implemented here should be + // abstracted out, there's some similar code from a library in + // the navigation plugin, this should be massaged a bit to fit + // the Flot cases here better and reused. Doing this would + // make this plugin much slimmer. + var savedhandlers = {}; + + var mouseUpHandler = null; + + function onMouseMove(e) { + if (selection.active) { + updateSelection(e); + + plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]); + } + } + + function onMouseDown(e) { + if (e.which != 1) // only accept left-click + return; + + // cancel out any text selections + document.body.focus(); + + // prevent text selection and drag in old-school browsers + if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) { + savedhandlers.onselectstart = document.onselectstart; + document.onselectstart = function () { return false; }; + } + if (document.ondrag !== undefined && savedhandlers.ondrag == null) { + savedhandlers.ondrag = document.ondrag; + document.ondrag = function () { return false; }; + } + + setSelectionPos(selection.first, e); + + selection.active = true; + + // this is a bit silly, but we have to use a closure to be + // able to whack the same handler again + mouseUpHandler = function (e) { onMouseUp(e); }; + + $(document).one("mouseup", mouseUpHandler); + } + + function onMouseUp(e) { + mouseUpHandler = null; + + // revert drag stuff for old-school browsers + if (document.onselectstart !== undefined) + document.onselectstart = savedhandlers.onselectstart; + if (document.ondrag !== undefined) + document.ondrag = savedhandlers.ondrag; + + // no more dragging + selection.active = false; + updateSelection(e); + + if (selectionIsSane()) + triggerSelectedEvent(); + else { + // this counts as a clear + plot.getPlaceholder().trigger("plotunselected", [ ]); + plot.getPlaceholder().trigger("plotselecting", [ null ]); + } + + return false; + } + + function getSelection() { + if (!selectionIsSane()) + return null; + + if (!selection.show) return null; + + var r = {}, c1 = selection.first, c2 = selection.second; + $.each(plot.getAxes(), function (name, axis) { + if (axis.used) { + var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]); + r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) }; + } + }); + return r; + } + + function triggerSelectedEvent() { + var r = getSelection(); + + plot.getPlaceholder().trigger("plotselected", [ r ]); + + // backwards-compat stuff, to be removed in future + if (r.xaxis && r.yaxis) + plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]); + } + + function clamp(min, value, max) { + return value < min ? min: (value > max ? max: value); + } + + function setSelectionPos(pos, e) { + var o = plot.getOptions(); + var offset = plot.getPlaceholder().offset(); + var plotOffset = plot.getPlotOffset(); + pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width()); + pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height()); + + if (o.selection.mode == "y") + pos.x = pos == selection.first ? 0 : plot.width(); + + if (o.selection.mode == "x") + pos.y = pos == selection.first ? 0 : plot.height(); + } + + function updateSelection(pos) { + if (pos.pageX == null) + return; + + setSelectionPos(selection.second, pos); + if (selectionIsSane()) { + selection.show = true; + plot.triggerRedrawOverlay(); + } + else + clearSelection(true); + } + + function clearSelection(preventEvent) { + if (selection.show) { + selection.show = false; + plot.triggerRedrawOverlay(); + if (!preventEvent) + plot.getPlaceholder().trigger("plotunselected", [ ]); + } + } + + // function taken from markings support in Flot + function extractRange(ranges, coord) { + var axis, from, to, key, axes = plot.getAxes(); + + for (var k in axes) { + axis = axes[k]; + if (axis.direction == coord) { + key = coord + axis.n + "axis"; + if (!ranges[key] && axis.n == 1) + key = coord + "axis"; // support x1axis as xaxis + if (ranges[key]) { + from = ranges[key].from; + to = ranges[key].to; + break; + } + } + } + + // backwards-compat stuff - to be removed in future + if (!ranges[key]) { + axis = coord == "x" ? plot.getXAxes()[0] : plot.getYAxes()[0]; + from = ranges[coord + "1"]; + to = ranges[coord + "2"]; + } + + // auto-reverse as an added bonus + if (from != null && to != null && from > to) { + var tmp = from; + from = to; + to = tmp; + } + + return { from: from, to: to, axis: axis }; + } + + function setSelection(ranges, preventEvent) { + var axis, range, o = plot.getOptions(); + + if (o.selection.mode == "y") { + selection.first.x = 0; + selection.second.x = plot.width(); + } + else { + range = extractRange(ranges, "x"); + + selection.first.x = range.axis.p2c(range.from); + selection.second.x = range.axis.p2c(range.to); + } + + if (o.selection.mode == "x") { + selection.first.y = 0; + selection.second.y = plot.height(); + } + else { + range = extractRange(ranges, "y"); + + selection.first.y = range.axis.p2c(range.from); + selection.second.y = range.axis.p2c(range.to); + } + + selection.show = true; + plot.triggerRedrawOverlay(); + if (!preventEvent && selectionIsSane()) + triggerSelectedEvent(); + } + + function selectionIsSane() { + var minSize = plot.getOptions().selection.minSize; + return Math.abs(selection.second.x - selection.first.x) >= minSize && + Math.abs(selection.second.y - selection.first.y) >= minSize; + } + + plot.clearSelection = clearSelection; + plot.setSelection = setSelection; + plot.getSelection = getSelection; + + plot.hooks.bindEvents.push(function(plot, eventHolder) { + var o = plot.getOptions(); + if (o.selection.mode != null) { + eventHolder.mousemove(onMouseMove); + eventHolder.mousedown(onMouseDown); + } + }); + + + plot.hooks.drawOverlay.push(function (plot, ctx) { + // draw selection + if (selection.show && selectionIsSane()) { + var plotOffset = plot.getPlotOffset(); + var o = plot.getOptions(); + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + var c = $.color.parse(o.selection.color); + + ctx.strokeStyle = c.scale('a', 0.8).toString(); + ctx.lineWidth = 1; + ctx.lineJoin = o.selection.shape; + ctx.fillStyle = c.scale('a', 0.4).toString(); + + var x = Math.min(selection.first.x, selection.second.x) + 0.5, + y = Math.min(selection.first.y, selection.second.y) + 0.5, + w = Math.abs(selection.second.x - selection.first.x) - 1, + h = Math.abs(selection.second.y - selection.first.y) - 1; + + ctx.fillRect(x, y, w, h); + ctx.strokeRect(x, y, w, h); + + ctx.restore(); + } + }); + + plot.hooks.shutdown.push(function (plot, eventHolder) { + eventHolder.unbind("mousemove", onMouseMove); + eventHolder.unbind("mousedown", onMouseDown); + + if (mouseUpHandler) + $(document).unbind("mouseup", mouseUpHandler); + }); + + } + + $.plot.plugins.push({ + init: init, + options: { + selection: { + mode: null, // one of null, "x", "y" or "xy" + color: "#e8cfac", + shape: "round", // one of "round", "miter", or "bevel" + minSize: 5 // minimum number of pixels + } + }, + name: 'selection', + version: '1.1' + }); +})(jQuery); diff --git a/src/plugins/timelion/public/webpackShims/jquery.flot.stack.js b/src/plugins/timelion/public/webpackShims/jquery.flot.stack.js new file mode 100644 index 0000000000000..0d91c0f3c0160 --- /dev/null +++ b/src/plugins/timelion/public/webpackShims/jquery.flot.stack.js @@ -0,0 +1,188 @@ +/* Flot plugin for stacking data sets rather than overlaying them. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The plugin assumes the data is sorted on x (or y if stacking horizontally). +For line charts, it is assumed that if a line has an undefined gap (from a +null point), then the line above it should have the same gap - insert zeros +instead of "null" if you want another behaviour. This also holds for the start +and end of the chart. Note that stacking a mix of positive and negative values +in most instances doesn't make sense (so it looks weird). + +Two or more series are stacked when their "stack" attribute is set to the same +key (which can be any number or string or just "true"). To specify the default +stack, you can set the stack option like this: + + series: { + stack: null/false, true, or a key (number/string) + } + +You can also specify it for a single series, like this: + + $.plot( $("#placeholder"), [{ + data: [ ... ], + stack: true + }]) + +The stacking order is determined by the order of the data series in the array +(later series end up on top of the previous). + +Internally, the plugin modifies the datapoints in each series, adding an +offset to the y value. For line series, extra data points are inserted through +interpolation. If there's a second y value, it's also adjusted (e.g for bar +charts or filled areas). + +*/ + +(function ($) { + var options = { + series: { stack: null } // or number/string + }; + + function init(plot) { + function findMatchingSeries(s, allseries) { + var res = null; + for (var i = 0; i < allseries.length; ++i) { + if (s == allseries[i]) + break; + + if (allseries[i].stack == s.stack) + res = allseries[i]; + } + + return res; + } + + function stackData(plot, s, datapoints) { + if (s.stack == null || s.stack === false) + return; + + var other = findMatchingSeries(s, plot.getData()); + if (!other) + return; + + var ps = datapoints.pointsize, + points = datapoints.points, + otherps = other.datapoints.pointsize, + otherpoints = other.datapoints.points, + newpoints = [], + px, py, intery, qx, qy, bottom, + withlines = s.lines.show, + horizontal = s.bars.horizontal, + withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y), + withsteps = withlines && s.lines.steps, + fromgap = true, + keyOffset = horizontal ? 1 : 0, + accumulateOffset = horizontal ? 0 : 1, + i = 0, j = 0, l, m; + + while (true) { + if (i >= points.length) + break; + + l = newpoints.length; + + if (points[i] == null) { + // copy gaps + for (m = 0; m < ps; ++m) + newpoints.push(points[i + m]); + i += ps; + } + else if (j >= otherpoints.length) { + // for lines, we can't use the rest of the points + if (!withlines) { + for (m = 0; m < ps; ++m) + newpoints.push(points[i + m]); + } + i += ps; + } + else if (otherpoints[j] == null) { + // oops, got a gap + for (m = 0; m < ps; ++m) + newpoints.push(null); + fromgap = true; + j += otherps; + } + else { + // cases where we actually got two points + px = points[i + keyOffset]; + py = points[i + accumulateOffset]; + qx = otherpoints[j + keyOffset]; + qy = otherpoints[j + accumulateOffset]; + bottom = 0; + + if (px == qx) { + for (m = 0; m < ps; ++m) + newpoints.push(points[i + m]); + + newpoints[l + accumulateOffset] += qy; + bottom = qy; + + i += ps; + j += otherps; + } + else if (px > qx) { + // we got past point below, might need to + // insert interpolated extra point + if (withlines && i > 0 && points[i - ps] != null) { + intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); + newpoints.push(qx); + newpoints.push(intery + qy); + for (m = 2; m < ps; ++m) + newpoints.push(points[i + m]); + bottom = qy; + } + + j += otherps; + } + else { // px < qx + if (fromgap && withlines) { + // if we come from a gap, we just skip this point + i += ps; + continue; + } + + for (m = 0; m < ps; ++m) + newpoints.push(points[i + m]); + + // we might be able to interpolate a point below, + // this can give us a better y + if (withlines && j > 0 && otherpoints[j - otherps] != null) + bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx); + + newpoints[l + accumulateOffset] += bottom; + + i += ps; + } + + fromgap = false; + + if (l != newpoints.length && withbottom) + newpoints[l + 2] += bottom; + } + + // maintain the line steps invariant + if (withsteps && l != newpoints.length && l > 0 + && newpoints[l] != null + && newpoints[l] != newpoints[l - ps] + && newpoints[l + 1] != newpoints[l - ps + 1]) { + for (m = 0; m < ps; ++m) + newpoints[l + ps + m] = newpoints[l + m]; + newpoints[l + 1] = newpoints[l - ps + 1]; + } + } + + datapoints.points = newpoints; + } + + plot.hooks.processDatapoints.push(stackData); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'stack', + version: '1.2' + }); +})(jQuery); diff --git a/src/plugins/timelion/public/webpackShims/jquery.flot.symbol.js b/src/plugins/timelion/public/webpackShims/jquery.flot.symbol.js new file mode 100644 index 0000000000000..79f634971b6fa --- /dev/null +++ b/src/plugins/timelion/public/webpackShims/jquery.flot.symbol.js @@ -0,0 +1,71 @@ +/* Flot plugin that adds some extra symbols for plotting points. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The symbols are accessed as strings through the standard symbol options: + + series: { + points: { + symbol: "square" // or "diamond", "triangle", "cross" + } + } + +*/ + +(function ($) { + function processRawData(plot, series, datapoints) { + // we normalize the area of each symbol so it is approximately the + // same as a circle of the given radius + + var handlers = { + square: function (ctx, x, y, radius, shadow) { + // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 + var size = radius * Math.sqrt(Math.PI) / 2; + ctx.rect(x - size, y - size, size + size, size + size); + }, + diamond: function (ctx, x, y, radius, shadow) { + // pi * r^2 = 2s^2 => s = r * sqrt(pi/2) + var size = radius * Math.sqrt(Math.PI / 2); + ctx.moveTo(x - size, y); + ctx.lineTo(x, y - size); + ctx.lineTo(x + size, y); + ctx.lineTo(x, y + size); + ctx.lineTo(x - size, y); + }, + triangle: function (ctx, x, y, radius, shadow) { + // pi * r^2 = 1/2 * s^2 * sin (pi / 3) => s = r * sqrt(2 * pi / sin(pi / 3)) + var size = radius * Math.sqrt(2 * Math.PI / Math.sin(Math.PI / 3)); + var height = size * Math.sin(Math.PI / 3); + ctx.moveTo(x - size/2, y + height/2); + ctx.lineTo(x + size/2, y + height/2); + if (!shadow) { + ctx.lineTo(x, y - height/2); + ctx.lineTo(x - size/2, y + height/2); + } + }, + cross: function (ctx, x, y, radius, shadow) { + // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 + var size = radius * Math.sqrt(Math.PI) / 2; + ctx.moveTo(x - size, y - size); + ctx.lineTo(x + size, y + size); + ctx.moveTo(x - size, y + size); + ctx.lineTo(x + size, y - size); + } + }; + + var s = series.points.symbol; + if (handlers[s]) + series.points.symbol = handlers[s]; + } + + function init(plot) { + plot.hooks.processDatapoints.push(processRawData); + } + + $.plot.plugins.push({ + init: init, + name: 'symbols', + version: '1.0' + }); +})(jQuery); diff --git a/src/plugins/timelion/public/webpackShims/jquery.flot.time.js b/src/plugins/timelion/public/webpackShims/jquery.flot.time.js new file mode 100644 index 0000000000000..34c1d121259a2 --- /dev/null +++ b/src/plugins/timelion/public/webpackShims/jquery.flot.time.js @@ -0,0 +1,432 @@ +/* Pretty handling of time axes. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +Set axis.mode to "time" to enable. See the section "Time series data" in +API.txt for details. + +*/ + +(function($) { + + var options = { + xaxis: { + timezone: null, // "browser" for local to the client or timezone for timezone-js + timeformat: null, // format string to use + twelveHourClock: false, // 12 or 24 time in time mode + monthNames: null // list of names of months + } + }; + + // round to nearby lower multiple of base + + function floorInBase(n, base) { + return base * Math.floor(n / base); + } + + // Returns a string with the date d formatted according to fmt. + // A subset of the Open Group's strftime format is supported. + + function formatDate(d, fmt, monthNames, dayNames) { + + if (typeof d.strftime == "function") { + return d.strftime(fmt); + } + + var leftPad = function(n, pad) { + n = "" + n; + pad = "" + (pad == null ? "0" : pad); + return n.length == 1 ? pad + n : n; + }; + + var r = []; + var escape = false; + var hours = d.getHours(); + var isAM = hours < 12; + + if (monthNames == null) { + monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + } + + if (dayNames == null) { + dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + } + + var hours12; + + if (hours > 12) { + hours12 = hours - 12; + } else if (hours == 0) { + hours12 = 12; + } else { + hours12 = hours; + } + + for (var i = 0; i < fmt.length; ++i) { + + var c = fmt.charAt(i); + + if (escape) { + switch (c) { + case 'a': c = "" + dayNames[d.getDay()]; break; + case 'b': c = "" + monthNames[d.getMonth()]; break; + case 'd': c = leftPad(d.getDate()); break; + case 'e': c = leftPad(d.getDate(), " "); break; + case 'h': // For back-compat with 0.7; remove in 1.0 + case 'H': c = leftPad(hours); break; + case 'I': c = leftPad(hours12); break; + case 'l': c = leftPad(hours12, " "); break; + case 'm': c = leftPad(d.getMonth() + 1); break; + case 'M': c = leftPad(d.getMinutes()); break; + // quarters not in Open Group's strftime specification + case 'q': + c = "" + (Math.floor(d.getMonth() / 3) + 1); break; + case 'S': c = leftPad(d.getSeconds()); break; + case 'y': c = leftPad(d.getFullYear() % 100); break; + case 'Y': c = "" + d.getFullYear(); break; + case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; + case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; + case 'w': c = "" + d.getDay(); break; + } + r.push(c); + escape = false; + } else { + if (c == "%") { + escape = true; + } else { + r.push(c); + } + } + } + + return r.join(""); + } + + // To have a consistent view of time-based data independent of which time + // zone the client happens to be in we need a date-like object independent + // of time zones. This is done through a wrapper that only calls the UTC + // versions of the accessor methods. + + function makeUtcWrapper(d) { + + function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) { + sourceObj[sourceMethod] = function() { + return targetObj[targetMethod].apply(targetObj, arguments); + }; + }; + + var utc = { + date: d + }; + + // support strftime, if found + + if (d.strftime != undefined) { + addProxyMethod(utc, "strftime", d, "strftime"); + } + + addProxyMethod(utc, "getTime", d, "getTime"); + addProxyMethod(utc, "setTime", d, "setTime"); + + var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"]; + + for (var p = 0; p < props.length; p++) { + addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]); + addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]); + } + + return utc; + }; + + // select time zone strategy. This returns a date-like object tied to the + // desired timezone + + function dateGenerator(ts, opts) { + if (opts.timezone == "browser") { + return new Date(ts); + } else if (!opts.timezone || opts.timezone == "utc") { + return makeUtcWrapper(new Date(ts)); + } else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") { + var d = new timezoneJS.Date(); + // timezone-js is fickle, so be sure to set the time zone before + // setting the time. + d.setTimezone(opts.timezone); + d.setTime(ts); + return d; + } else { + return makeUtcWrapper(new Date(ts)); + } + } + + // map of app. size of time units in milliseconds + + var timeUnitSize = { + "second": 1000, + "minute": 60 * 1000, + "hour": 60 * 60 * 1000, + "day": 24 * 60 * 60 * 1000, + "month": 30 * 24 * 60 * 60 * 1000, + "quarter": 3 * 30 * 24 * 60 * 60 * 1000, + "year": 365.2425 * 24 * 60 * 60 * 1000 + }; + + // the allowed tick sizes, after 1 year we use + // an integer algorithm + + var baseSpec = [ + [1, "second"], [2, "second"], [5, "second"], [10, "second"], + [30, "second"], + [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], + [30, "minute"], + [1, "hour"], [2, "hour"], [4, "hour"], + [8, "hour"], [12, "hour"], + [1, "day"], [2, "day"], [3, "day"], + [0.25, "month"], [0.5, "month"], [1, "month"], + [2, "month"] + ]; + + // we don't know which variant(s) we'll need yet, but generating both is + // cheap + + var specMonths = baseSpec.concat([[3, "month"], [6, "month"], + [1, "year"]]); + var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"], + [1, "year"]]); + + function init(plot) { + plot.hooks.processOptions.push(function (plot, options) { + $.each(plot.getAxes(), function(axisName, axis) { + + var opts = axis.options; + + if (opts.mode == "time") { + axis.tickGenerator = function(axis) { + + var ticks = []; + var d = dateGenerator(axis.min, opts); + var minSize = 0; + + // make quarter use a possibility if quarters are + // mentioned in either of these options + + var spec = (opts.tickSize && opts.tickSize[1] === + "quarter") || + (opts.minTickSize && opts.minTickSize[1] === + "quarter") ? specQuarters : specMonths; + + if (opts.minTickSize != null) { + if (typeof opts.tickSize == "number") { + minSize = opts.tickSize; + } else { + minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; + } + } + + for (var i = 0; i < spec.length - 1; ++i) { + if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]] + + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 + && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) { + break; + } + } + + var size = spec[i][0]; + var unit = spec[i][1]; + + // special-case the possibility of several years + + if (unit == "year") { + + // if given a minTickSize in years, just use it, + // ensuring that it's an integer + + if (opts.minTickSize != null && opts.minTickSize[1] == "year") { + size = Math.floor(opts.minTickSize[0]); + } else { + + var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10)); + var norm = (axis.delta / timeUnitSize.year) / magn; + + if (norm < 1.5) { + size = 1; + } else if (norm < 3) { + size = 2; + } else if (norm < 7.5) { + size = 5; + } else { + size = 10; + } + + size *= magn; + } + + // minimum size for years is 1 + + if (size < 1) { + size = 1; + } + } + + axis.tickSize = opts.tickSize || [size, unit]; + var tickSize = axis.tickSize[0]; + unit = axis.tickSize[1]; + + var step = tickSize * timeUnitSize[unit]; + + if (unit == "second") { + d.setSeconds(floorInBase(d.getSeconds(), tickSize)); + } else if (unit == "minute") { + d.setMinutes(floorInBase(d.getMinutes(), tickSize)); + } else if (unit == "hour") { + d.setHours(floorInBase(d.getHours(), tickSize)); + } else if (unit == "month") { + d.setMonth(floorInBase(d.getMonth(), tickSize)); + } else if (unit == "quarter") { + d.setMonth(3 * floorInBase(d.getMonth() / 3, + tickSize)); + } else if (unit == "year") { + d.setFullYear(floorInBase(d.getFullYear(), tickSize)); + } + + // reset smaller components + + d.setMilliseconds(0); + + if (step >= timeUnitSize.minute) { + d.setSeconds(0); + } + if (step >= timeUnitSize.hour) { + d.setMinutes(0); + } + if (step >= timeUnitSize.day) { + d.setHours(0); + } + if (step >= timeUnitSize.day * 4) { + d.setDate(1); + } + if (step >= timeUnitSize.month * 2) { + d.setMonth(floorInBase(d.getMonth(), 3)); + } + if (step >= timeUnitSize.quarter * 2) { + d.setMonth(floorInBase(d.getMonth(), 6)); + } + if (step >= timeUnitSize.year) { + d.setMonth(0); + } + + var carry = 0; + var v = Number.NaN; + var prev; + + do { + + prev = v; + v = d.getTime(); + ticks.push(v); + + if (unit == "month" || unit == "quarter") { + if (tickSize < 1) { + + // a bit complicated - we'll divide the + // month/quarter up but we need to take + // care of fractions so we don't end up in + // the middle of a day + + d.setDate(1); + var start = d.getTime(); + d.setMonth(d.getMonth() + + (unit == "quarter" ? 3 : 1)); + var end = d.getTime(); + d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); + carry = d.getHours(); + d.setHours(0); + } else { + d.setMonth(d.getMonth() + + tickSize * (unit == "quarter" ? 3 : 1)); + } + } else if (unit == "year") { + d.setFullYear(d.getFullYear() + tickSize); + } else { + d.setTime(v + step); + } + } while (v < axis.max && v != prev); + + return ticks; + }; + + axis.tickFormatter = function (v, axis) { + + var d = dateGenerator(v, axis.options); + + // first check global format + + if (opts.timeformat != null) { + return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames); + } + + // possibly use quarters if quarters are mentioned in + // any of these places + + var useQuarters = (axis.options.tickSize && + axis.options.tickSize[1] == "quarter") || + (axis.options.minTickSize && + axis.options.minTickSize[1] == "quarter"); + + var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; + var span = axis.max - axis.min; + var suffix = (opts.twelveHourClock) ? " %p" : ""; + var hourCode = (opts.twelveHourClock) ? "%I" : "%H"; + var fmt; + + if (t < timeUnitSize.minute) { + fmt = hourCode + ":%M:%S" + suffix; + } else if (t < timeUnitSize.day) { + if (span < 2 * timeUnitSize.day) { + fmt = hourCode + ":%M" + suffix; + } else { + fmt = "%b %d " + hourCode + ":%M" + suffix; + } + } else if (t < timeUnitSize.month) { + fmt = "%b %d"; + } else if ((useQuarters && t < timeUnitSize.quarter) || + (!useQuarters && t < timeUnitSize.year)) { + if (span < timeUnitSize.year) { + fmt = "%b"; + } else { + fmt = "%b %Y"; + } + } else if (useQuarters && t < timeUnitSize.year) { + if (span < timeUnitSize.year) { + fmt = "Q%q"; + } else { + fmt = "Q%q %Y"; + } + } else { + fmt = "%Y"; + } + + var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames); + + return rt; + }; + } + }); + }); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'time', + version: '1.0' + }); + + // Time-axis support used to be in Flot core, which exposed the + // formatDate function on the plot object. Various plugins depend + // on the function, so we need to re-expose it here. + + $.plot.formatDate = formatDate; + $.plot.dateGenerator = dateGenerator; + +})(jQuery); diff --git a/src/legacy/core_plugins/timelion/public/components/timelionhelp_tabs_directive.js b/src/plugins/timelion/server/config.ts similarity index 67% rename from src/legacy/core_plugins/timelion/public/components/timelionhelp_tabs_directive.js rename to src/plugins/timelion/server/config.ts index 7e77027f750c6..16e559761e9ad 100644 --- a/src/legacy/core_plugins/timelion/public/components/timelionhelp_tabs_directive.js +++ b/src/plugins/timelion/server/config.ts @@ -17,14 +17,16 @@ * under the License. */ -import 'ngreact'; +import { schema, TypeOf } from '@kbn/config-schema'; -import { wrapInI18nContext } from 'ui/i18n'; -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/timelion', ['react']); +export const configSchema = { + schema: schema.object({ + graphiteUrls: schema.maybe(schema.arrayOf(schema.string())), + enabled: schema.boolean({ defaultValue: true }), + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + }), +}; -import { TimelionHelpTabs } from './timelionhelp_tabs'; - -module.directive('timelionHelpTabs', function (reactDirective) { - return reactDirective(wrapInI18nContext(TimelionHelpTabs), undefined, { restrict: 'E' }); -}); +export type TimelionConfigType = TypeOf; diff --git a/src/plugins/timelion/server/index.ts b/src/plugins/timelion/server/index.ts index 5bb0c9e2567e0..28c5709d89132 100644 --- a/src/plugins/timelion/server/index.ts +++ b/src/plugins/timelion/server/index.ts @@ -16,7 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; import { TimelionPlugin } from './plugin'; +import { configSchema, TimelionConfigType } from './config'; -export const plugin = (context: PluginInitializerContext) => new TimelionPlugin(context); +export const config: PluginConfigDescriptor = { + schema: configSchema.schema, +}; + +export const plugin = (context: PluginInitializerContext) => + new TimelionPlugin(context); diff --git a/src/plugins/timelion/server/plugin.ts b/src/plugins/timelion/server/plugin.ts index 015f0c573e531..3e4cd5467dd44 100644 --- a/src/plugins/timelion/server/plugin.ts +++ b/src/plugins/timelion/server/plugin.ts @@ -16,12 +16,21 @@ * specific language governing permissions and limitations * under the License. */ + import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/server'; +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import { TimelionConfigType } from './config'; export class TimelionPlugin implements Plugin { - constructor(context: PluginInitializerContext) {} + constructor(context: PluginInitializerContext) {} - setup(core: CoreSetup) { + public setup(core: CoreSetup) { + core.capabilities.registerProvider(() => ({ + timelion: { + save: true, + }, + })); core.savedObjects.registerType({ name: 'timelion-sheet', hidden: false, @@ -46,6 +55,42 @@ export class TimelionPlugin implements Plugin { }, }, }); + + core.uiSettings.register({ + 'timelion:showTutorial': { + name: i18n.translate('timelion.uiSettings.showTutorialLabel', { + defaultMessage: 'Show tutorial', + }), + value: false, + description: i18n.translate('timelion.uiSettings.showTutorialDescription', { + defaultMessage: 'Should I show the tutorial by default when entering the timelion app?', + }), + category: ['timelion'], + schema: schema.boolean(), + }, + 'timelion:default_columns': { + name: i18n.translate('timelion.uiSettings.defaultColumnsLabel', { + defaultMessage: 'Default columns', + }), + value: 2, + description: i18n.translate('timelion.uiSettings.defaultColumnsDescription', { + defaultMessage: 'Number of columns on a timelion sheet by default', + }), + category: ['timelion'], + schema: schema.number(), + }, + 'timelion:default_rows': { + name: i18n.translate('timelion.uiSettings.defaultRowsLabel', { + defaultMessage: 'Default rows', + }), + value: 2, + description: i18n.translate('timelion.uiSettings.defaultRowsDescription', { + defaultMessage: 'Number of rows on a timelion sheet by default', + }), + category: ['timelion'], + schema: schema.number(), + }, + }); } start() {} stop() {} diff --git a/src/plugins/vis_type_timelion/public/index.ts b/src/plugins/vis_type_timelion/public/index.ts index 0aa5f3a810033..abfe345d8c672 100644 --- a/src/plugins/vis_type_timelion/public/index.ts +++ b/src/plugins/vis_type_timelion/public/index.ts @@ -25,5 +25,10 @@ export function plugin(initializerContext: PluginInitializerContext) { } export { getTimezone } from './helpers/get_timezone'; +export { tickFormatters } from './helpers/tick_formatters'; +export { xaxisFormatterProvider } from './helpers/xaxis_formatter'; +export { generateTicksProvider } from './helpers/tick_generator'; + +export { DEFAULT_TIME_FORMAT, calculateInterval } from '../common/lib'; export { VisTypeTimelionPluginStart } from './plugin'; diff --git a/src/plugins/vis_type_timelion/server/plugin.ts b/src/plugins/vis_type_timelion/server/plugin.ts index 605c6be0a85df..5e6557e305692 100644 --- a/src/plugins/vis_type_timelion/server/plugin.ts +++ b/src/plugins/vis_type_timelion/server/plugin.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { first } from 'rxjs/operators'; -import { TypeOf } from '@kbn/config-schema'; +import { TypeOf, schema } from '@kbn/config-schema'; import { RecursiveReadonly } from '@kbn/utility-types'; import { CoreSetup, PluginInitializerContext } from '../../../../src/core/server'; @@ -31,6 +31,10 @@ import { validateEsRoute } from './routes/validate_es'; import { runRoute } from './routes/run'; import { ConfigManager } from './lib/config_manager'; +const experimentalLabel = i18n.translate('timelion.uiSettings.experimentalLabel', { + defaultMessage: 'experimental', +}); + /** * Describes public Timelion plugin contract returned at the `setup` stage. */ @@ -82,6 +86,97 @@ export class Plugin { runRoute(router, deps); validateEsRoute(router); + core.uiSettings.register({ + 'timelion:es.timefield': { + name: i18n.translate('timelion.uiSettings.timeFieldLabel', { + defaultMessage: 'Time field', + }), + value: '@timestamp', + description: i18n.translate('timelion.uiSettings.timeFieldDescription', { + defaultMessage: 'Default field containing a timestamp when using {esParam}', + values: { esParam: '.es()' }, + }), + category: ['timelion'], + schema: schema.string(), + }, + 'timelion:es.default_index': { + name: i18n.translate('timelion.uiSettings.defaultIndexLabel', { + defaultMessage: 'Default index', + }), + value: '_all', + description: i18n.translate('timelion.uiSettings.defaultIndexDescription', { + defaultMessage: 'Default elasticsearch index to search with {esParam}', + values: { esParam: '.es()' }, + }), + category: ['timelion'], + schema: schema.string(), + }, + 'timelion:target_buckets': { + name: i18n.translate('timelion.uiSettings.targetBucketsLabel', { + defaultMessage: 'Target buckets', + }), + value: 200, + description: i18n.translate('timelion.uiSettings.targetBucketsDescription', { + defaultMessage: 'The number of buckets to shoot for when using auto intervals', + }), + category: ['timelion'], + schema: schema.number(), + }, + 'timelion:max_buckets': { + name: i18n.translate('timelion.uiSettings.maximumBucketsLabel', { + defaultMessage: 'Maximum buckets', + }), + value: 2000, + description: i18n.translate('timelion.uiSettings.maximumBucketsDescription', { + defaultMessage: 'The maximum number of buckets a single datasource can return', + }), + category: ['timelion'], + schema: schema.number(), + }, + 'timelion:min_interval': { + name: i18n.translate('timelion.uiSettings.minimumIntervalLabel', { + defaultMessage: 'Minimum interval', + }), + value: '1ms', + description: i18n.translate('timelion.uiSettings.minimumIntervalDescription', { + defaultMessage: 'The smallest interval that will be calculated when using "auto"', + description: + '"auto" is a technical value in that context, that should not be translated.', + }), + category: ['timelion'], + schema: schema.string(), + }, + 'timelion:graphite.url': { + name: i18n.translate('timelion.uiSettings.graphiteURLLabel', { + defaultMessage: 'Graphite URL', + description: + 'The URL should be in the form of https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite', + }), + value: config.graphiteUrls && config.graphiteUrls.length ? config.graphiteUrls[0] : null, + description: i18n.translate('timelion.uiSettings.graphiteURLDescription', { + defaultMessage: + '{experimentalLabel} The URL of your graphite host', + values: { experimentalLabel: `[${experimentalLabel}]` }, + }), + type: 'select', + options: config.graphiteUrls || [], + category: ['timelion'], + schema: schema.nullable(schema.string()), + }, + 'timelion:quandl.key': { + name: i18n.translate('timelion.uiSettings.quandlKeyLabel', { + defaultMessage: 'Quandl key', + }), + value: 'someKeyHere', + description: i18n.translate('timelion.uiSettings.quandlKeyDescription', { + defaultMessage: '{experimentalLabel} Your API key from www.quandl.com', + values: { experimentalLabel: `[${experimentalLabel}]` }, + }), + category: ['timelion'], + schema: schema.string(), + }, + }); + return deepFreeze({ uiEnabled: config.ui.enabled }); } diff --git a/src/test_utils/public/key_map.ts b/src/test_utils/public/key_map.ts new file mode 100644 index 0000000000000..aac3c6b2db3e0 --- /dev/null +++ b/src/test_utils/public/key_map.ts @@ -0,0 +1,121 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const keyMap: { [key: number]: string } = { + 8: 'backspace', + 9: 'tab', + 13: 'enter', + 16: 'shift', + 17: 'ctrl', + 18: 'alt', + 19: 'pause', + 20: 'capsLock', + 27: 'escape', + 32: 'space', + 33: 'pageUp', + 34: 'pageDown', + 35: 'end', + 36: 'home', + 37: 'left', + 38: 'up', + 39: 'right', + 40: 'down', + 45: 'insert', + 46: 'delete', + 48: '0', + 49: '1', + 50: '2', + 51: '3', + 52: '4', + 53: '5', + 54: '6', + 55: '7', + 56: '8', + 57: '9', + 65: 'a', + 66: 'b', + 67: 'c', + 68: 'd', + 69: 'e', + 70: 'f', + 71: 'g', + 72: 'h', + 73: 'i', + 74: 'j', + 75: 'k', + 76: 'l', + 77: 'm', + 78: 'n', + 79: 'o', + 80: 'p', + 81: 'q', + 82: 'r', + 83: 's', + 84: 't', + 85: 'u', + 86: 'v', + 87: 'w', + 88: 'x', + 89: 'y', + 90: 'z', + 91: 'leftWindowKey', + 92: 'rightWindowKey', + 93: 'selectKey', + 96: '0', + 97: '1', + 98: '2', + 99: '3', + 100: '4', + 101: '5', + 102: '6', + 103: '7', + 104: '8', + 105: '9', + 106: 'multiply', + 107: 'add', + 109: 'subtract', + 110: 'period', + 111: 'divide', + 112: 'f1', + 113: 'f2', + 114: 'f3', + 115: 'f4', + 116: 'f5', + 117: 'f6', + 118: 'f7', + 119: 'f8', + 120: 'f9', + 121: 'f10', + 122: 'f11', + 123: 'f12', + 144: 'numLock', + 145: 'scrollLock', + 186: 'semiColon', + 187: 'equalSign', + 188: 'comma', + 189: 'dash', + 190: 'period', + 191: 'forwardSlash', + 192: 'graveAccent', + 219: 'openBracket', + 220: 'backSlash', + 221: 'closeBracket', + 222: 'singleQuote', + 224: 'meta', +}; diff --git a/src/test_utils/public/simulate_keys.js b/src/test_utils/public/simulate_keys.js index 56596508a2181..460a75486169a 100644 --- a/src/test_utils/public/simulate_keys.js +++ b/src/test_utils/public/simulate_keys.js @@ -20,7 +20,7 @@ import $ from 'jquery'; import _ from 'lodash'; import Bluebird from 'bluebird'; -import { keyMap } from 'ui/directives/key_map'; +import { keyMap } from './key_map'; const reverseKeyMap = _.mapValues(_.invert(keyMap), _.ary(_.parseInt, 1)); /** From 385e4d0a21be60b0100715863d4bf23a4fc575df Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 14 Jul 2020 09:56:05 -0600 Subject: [PATCH 06/82] [Maps] expose registerLayerWizard and registerSource in maps plugin start (#71553) --- x-pack/plugins/maps/public/api/index.ts | 2 ++ x-pack/plugins/maps/public/api/register.ts | 19 +++++++++++++++++++ x-pack/plugins/maps/public/api/start_api.ts | 4 ++++ .../public/classes/sources/source_registry.ts | 2 +- .../maps/public/lazy_load_bundle/index.ts | 8 ++++++++ .../public/lazy_load_bundle/lazy/index.ts | 2 ++ x-pack/plugins/maps/public/plugin.ts | 4 +++- 7 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/maps/public/api/register.ts diff --git a/x-pack/plugins/maps/public/api/index.ts b/x-pack/plugins/maps/public/api/index.ts index 8b45d31b41d44..ec5aa124fb7f9 100644 --- a/x-pack/plugins/maps/public/api/index.ts +++ b/x-pack/plugins/maps/public/api/index.ts @@ -5,3 +5,5 @@ */ export { MapsStartApi } from './start_api'; +export { createSecurityLayerDescriptors } from './create_security_layer_descriptors'; +export { registerLayerWizard, registerSource } from './register'; diff --git a/x-pack/plugins/maps/public/api/register.ts b/x-pack/plugins/maps/public/api/register.ts new file mode 100644 index 0000000000000..4846b6a198c71 --- /dev/null +++ b/x-pack/plugins/maps/public/api/register.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SourceRegistryEntry } from '../classes/sources/source_registry'; +import { LayerWizard } from '../classes/layers/layer_wizard_registry'; +import { lazyLoadMapModules } from '../lazy_load_bundle'; + +export async function registerLayerWizard(layerWizard: LayerWizard): Promise { + const mapModules = await lazyLoadMapModules(); + return mapModules.registerLayerWizard(layerWizard); +} + +export async function registerSource(entry: SourceRegistryEntry): Promise { + const mapModules = await lazyLoadMapModules(); + return mapModules.registerSource(entry); +} diff --git a/x-pack/plugins/maps/public/api/start_api.ts b/x-pack/plugins/maps/public/api/start_api.ts index d45b0df63c839..32db3bc771a3b 100644 --- a/x-pack/plugins/maps/public/api/start_api.ts +++ b/x-pack/plugins/maps/public/api/start_api.ts @@ -5,10 +5,14 @@ */ import { LayerDescriptor } from '../../common/descriptor_types'; +import { SourceRegistryEntry } from '../classes/sources/source_registry'; +import { LayerWizard } from '../classes/layers/layer_wizard_registry'; export interface MapsStartApi { createSecurityLayerDescriptors: ( indexPatternId: string, indexPatternTitle: string ) => Promise; + registerLayerWizard(layerWizard: LayerWizard): Promise; + registerSource(entry: SourceRegistryEntry): Promise; } diff --git a/x-pack/plugins/maps/public/classes/sources/source_registry.ts b/x-pack/plugins/maps/public/classes/sources/source_registry.ts index 3b334d45092ad..462624dfa6ec9 100644 --- a/x-pack/plugins/maps/public/classes/sources/source_registry.ts +++ b/x-pack/plugins/maps/public/classes/sources/source_registry.ts @@ -7,7 +7,7 @@ import { ISource } from './source'; -type SourceRegistryEntry = { +export type SourceRegistryEntry = { ConstructorFunction: new ( sourceDescriptor: any, // this is the source-descriptor that corresponds specifically to the particular ISource instance inspectorAdapters?: object diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts index ca4098ebfa805..12d6d75ac57ba 100644 --- a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts +++ b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts @@ -14,6 +14,8 @@ import { MapStore, MapStoreState } from '../reducers/store'; import { EventHandlers } from '../reducers/non_serializable_instances'; import { RenderToolTipContent } from '../classes/tooltips/tooltip_property'; import { MapEmbeddableConfig, MapEmbeddableInput, MapEmbeddableOutput } from '../embeddable/types'; +import { SourceRegistryEntry } from '../classes/sources/source_registry'; +import { LayerWizard } from '../classes/layers/layer_wizard_registry'; let loadModulesPromise: Promise; @@ -42,6 +44,8 @@ interface LazyLoadedMapModules { indexPatternId: string, indexPatternTitle: string ) => LayerDescriptor[]; + registerLayerWizard(layerWizard: LayerWizard): void; + registerSource(entry: SourceRegistryEntry): void; } export async function lazyLoadMapModules(): Promise { @@ -65,6 +69,8 @@ export async function lazyLoadMapModules(): Promise { // @ts-expect-error renderApp, createSecurityLayerDescriptors, + registerLayerWizard, + registerSource, } = await import('./lazy'); resolve({ @@ -80,6 +86,8 @@ export async function lazyLoadMapModules(): Promise { mergeInputWithSavedMap, renderApp, createSecurityLayerDescriptors, + registerLayerWizard, + registerSource, }); }); return loadModulesPromise; diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts index 4f9f01f8a1b37..c839122ab90b1 100644 --- a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts +++ b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts @@ -19,3 +19,5 @@ export * from '../../embeddable/merge_input_with_saved_map'; // @ts-expect-error export * from '../../routing/maps_router'; export * from '../../classes/layers/solution_layers/security'; +export { registerLayerWizard } from '../../classes/layers/layer_wizard_registry'; +export { registerSource } from '../../classes/sources/source_registry'; diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 412e8832453bc..8428a31d8b408 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -55,7 +55,7 @@ import { getAppTitle } from '../common/i18n_getters'; import { ILicense } from '../../licensing/common/types'; import { lazyLoadMapModules } from './lazy_load_bundle'; import { MapsStartApi } from './api'; -import { createSecurityLayerDescriptors } from './api/create_security_layer_descriptors'; +import { createSecurityLayerDescriptors, registerLayerWizard, registerSource } from './api'; export interface MapsPluginSetupDependencies { inspector: InspectorSetupContract; @@ -170,6 +170,8 @@ export class MapsPlugin bindStartCoreAndPlugins(core, plugins); return { createSecurityLayerDescriptors, + registerLayerWizard, + registerSource, }; } } From 301d9cecf6bb52abf280cced144cf9e6054c4ca1 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 14 Jul 2020 09:58:14 -0600 Subject: [PATCH 07/82] [Maps] get isClustered from count request instead of source data request (#71528) * [Maps] get isClustered from count request instead of source data request * better naming * tslint * review feedback --- .../data_request_descriptor_types.ts | 4 +--- .../blended_vector_layer.ts | 20 +++++++++---------- .../es_geo_grid_source/es_geo_grid_source.js | 1 - .../es_search_source/es_search_source.js | 1 - 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts index 1bd8c5401eb1d..35b33da12d384 100644 --- a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts @@ -5,7 +5,7 @@ */ /* eslint-disable @typescript-eslint/consistent-type-definitions */ -import { RENDER_AS, SORT_ORDER, SCALING_TYPES, SOURCE_TYPES } from '../constants'; +import { RENDER_AS, SORT_ORDER, SCALING_TYPES } from '../constants'; import { MapExtent, MapQuery } from './map_descriptor'; import { Filter, TimeRange } from '../../../../../src/plugins/data/common'; @@ -26,12 +26,10 @@ type ESSearchSourceSyncMeta = { scalingType: SCALING_TYPES; topHitsSplitField: string; topHitsSize: number; - sourceType: SOURCE_TYPES.ES_SEARCH; }; type ESGeoGridSourceSyncMeta = { requestType: RENDER_AS; - sourceType: SOURCE_TYPES.ES_GEO_GRID; }; export type VectorSourceSyncMeta = ESSearchSourceSyncMeta | ESGeoGridSourceSyncMeta | null; diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts index 26a0ffc1b1a37..5388a82e5924d 100644 --- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts @@ -11,7 +11,6 @@ import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_de import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; import { IStyleProperty } from '../../styles/vector/properties/style_property'; import { - SOURCE_TYPES, COUNT_PROP_LABEL, COUNT_PROP_NAME, LAYER_TYPE, @@ -41,6 +40,10 @@ import { IVectorSource } from '../../sources/vector_source'; const ACTIVE_COUNT_DATA_ID = 'ACTIVE_COUNT_DATA_ID'; +interface CountData { + isSyncClustered: boolean; +} + function getAggType(dynamicProperty: IDynamicStyleProperty): AGG_TYPE { return dynamicProperty.isOrdinal() ? AGG_TYPE.AVG : AGG_TYPE.TERMS; } @@ -187,14 +190,10 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { this._clusterStyle = new VectorStyle(clusterStyleDescriptor, this._clusterSource, this); let isClustered = false; - const sourceDataRequest = this.getSourceDataRequest(); - if (sourceDataRequest) { - const requestMeta = sourceDataRequest.getMeta(); - if ( - requestMeta && - requestMeta.sourceMeta && - requestMeta.sourceMeta.sourceType === SOURCE_TYPES.ES_GEO_GRID - ) { + const countDataRequest = this.getDataRequest(ACTIVE_COUNT_DATA_ID); + if (countDataRequest) { + const requestData = countDataRequest.getData() as CountData; + if (requestData && requestData.isSyncClustered) { isClustered = true; } } @@ -284,7 +283,8 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { const resp = await searchSource.fetch(); const maxResultWindow = await this._documentSource.getMaxResultWindow(); isSyncClustered = resp.hits.total > maxResultWindow; - syncContext.stopLoading(dataRequestId, requestToken, { isSyncClustered }, searchFilters); + const countData = { isSyncClustered } as CountData; + syncContext.stopLoading(dataRequestId, requestToken, countData, searchFilters); } catch (error) { if (!(error instanceof DataRequestAbortError)) { syncContext.onLoadError(dataRequestId, requestToken, error.message); diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js index 1be74140fe1bf..3902709eeb841 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js @@ -63,7 +63,6 @@ export class ESGeoGridSource extends AbstractESAggSource { getSyncMeta() { return { requestType: this._descriptor.requestType, - sourceType: SOURCE_TYPES.ES_GEO_GRID, }; } diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js index 330fa6e8318ed..256becf70ffb0 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js @@ -540,7 +540,6 @@ export class ESSearchSource extends AbstractESSource { scalingType: this._descriptor.scalingType, topHitsSplitField: this._descriptor.topHitsSplitField, topHitsSize: this._descriptor.topHitsSize, - sourceType: SOURCE_TYPES.ES_SEARCH, }; } From f0787f122467de0ee537ba5009833cae39b82a81 Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Tue, 14 Jul 2020 09:58:47 -0600 Subject: [PATCH 08/82] Fix coordinate maps layers dropdown (#70609) --- src/plugins/tile_map/public/plugin.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/plugins/tile_map/public/plugin.ts b/src/plugins/tile_map/public/plugin.ts index 1f79104b183ee..4582cd2283dc1 100644 --- a/src/plugins/tile_map/public/plugin.ts +++ b/src/plugins/tile_map/public/plugin.ts @@ -32,7 +32,7 @@ import 'angular-sanitize'; import { createTileMapFn } from './tile_map_fn'; // @ts-ignore import { createTileMapTypeDefinition } from './tile_map_type'; -import { MapsLegacyPluginSetup } from '../../maps_legacy/public'; +import { IServiceSettings, MapsLegacyPluginSetup } from '../../maps_legacy/public'; import { DataPublicPluginStart } from '../../data/public'; import { setFormatService, setQueryService } from './services'; import { setKibanaLegacy } from './services'; @@ -48,6 +48,7 @@ interface TileMapVisualizationDependencies { getZoomPrecision: any; getPrecision: any; BaseMapsVisualization: any; + serviceSettings: IServiceSettings; } /** @internal */ @@ -81,12 +82,13 @@ export class TileMapPlugin implements Plugin = { getZoomPrecision, getPrecision, BaseMapsVisualization: mapsLegacy.getBaseMapsVis(), uiSettings: core.uiSettings, + serviceSettings, }; expressions.registerFunction(() => createTileMapFn(visualizationDependencies)); From 4e97bb11eb74ac5686e71970dbf8ec77afc4caa0 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 14 Jul 2020 18:13:20 +0200 Subject: [PATCH 09/82] [Graph] Ensure inclusive language (#71416) --- docs/management/advanced-options.asciidoc | 2 +- .../public/angular/graph_client_workspace.js | 28 +++++++-------- .../angular/graph_client_workspace.test.js | 4 +-- .../graph/public/angular/templates/index.html | 2 +- x-pack/plugins/graph/public/app.js | 4 +-- ...{blacklist_form.tsx => blocklist_form.tsx} | 34 +++++++++---------- .../components/settings/settings.test.tsx | 30 ++++++++-------- .../public/components/settings/settings.tsx | 12 +++---- .../services/persistence/deserialize.test.ts | 4 +-- .../services/persistence/deserialize.ts | 10 +++--- .../services/persistence/serialize.test.ts | 4 +-- .../public/services/persistence/serialize.ts | 6 ++-- .../graph/public/state_management/mocks.ts | 2 +- .../public/state_management/persistence.ts | 2 +- .../plugins/graph/public/types/persistence.ts | 2 +- .../graph/public/types/workspace_state.ts | 2 +- .../graph/server/sample_data/ecommerce.ts | 2 +- .../graph/server/sample_data/flights.ts | 2 +- .../plugins/graph/server/sample_data/logs.ts | 2 +- .../graph/server/saved_objects/migrations.ts | 19 +++++++++++ .../translations/translations/ja-JP.json | 10 +++--- .../translations/translations/zh-CN.json | 10 +++--- 22 files changed, 106 insertions(+), 87 deletions(-) rename x-pack/plugins/graph/public/components/settings/{blacklist_form.tsx => blocklist_form.tsx} (72%) diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 561919738786e..9a94c25bcdf6e 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -247,7 +247,7 @@ retrieved. `timelion:es.timefield`:: The default field containing a timestamp when using the `.es()` query. `timelion:graphite.url`:: [experimental] Used with graphite queries, this is the URL of your graphite host in the form https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite. This URL can be -selected from a whitelist configured in the `kibana.yml` under `timelion.graphiteUrls`. +selected from an allow-list configured in the `kibana.yml` under `timelion.graphiteUrls`. `timelion:max_buckets`:: The maximum number of buckets a single data source can return. This value is used for calculating automatic intervals in visualizations. `timelion:min_interval`:: The smallest interval to calculate when using "auto". diff --git a/x-pack/plugins/graph/public/angular/graph_client_workspace.js b/x-pack/plugins/graph/public/angular/graph_client_workspace.js index cfa125fcc49ee..5cc06bad4c423 100644 --- a/x-pack/plugins/graph/public/angular/graph_client_workspace.js +++ b/x-pack/plugins/graph/public/angular/graph_client_workspace.js @@ -107,7 +107,7 @@ function UnGroupOperation(parent, child) { // The main constructor for our GraphWorkspace function GraphWorkspace(options) { const self = this; - this.blacklistedNodes = []; + this.blocklistedNodes = []; this.options = options; this.undoLog = []; this.redoLog = []; @@ -379,7 +379,7 @@ function GraphWorkspace(options) { this.redoLog = []; this.nodesMap = {}; this.edgesMap = {}; - this.blacklistedNodes = []; + this.blocklistedNodes = []; this.selectedNodes = []; this.lastResponse = null; }; @@ -630,11 +630,11 @@ function GraphWorkspace(options) { self.runLayout(); }; - this.unblacklist = function (node) { - self.arrRemove(self.blacklistedNodes, node); + this.unblocklist = function (node) { + self.arrRemove(self.blocklistedNodes, node); }; - this.blacklistSelection = function () { + this.blocklistSelection = function () { const selection = self.getAllSelectedNodes(); const danglingEdges = []; self.edges.forEach(function (edge) { @@ -645,7 +645,7 @@ function GraphWorkspace(options) { }); selection.forEach((node) => { delete self.nodesMap[node.id]; - self.blacklistedNodes.push(node); + self.blocklistedNodes.push(node); node.isSelected = false; }); self.arrRemoveAll(self.nodes, selection); @@ -671,10 +671,10 @@ function GraphWorkspace(options) { } let step = {}; - //Add any blacklisted nodes to exclusion list + //Add any blocklisted nodes to exclusion list const excludeNodesByField = {}; const nots = []; - const avoidNodes = this.blacklistedNodes; + const avoidNodes = this.blocklistedNodes; for (let i = 0; i < avoidNodes.length; i++) { const n = avoidNodes[i]; let arr = excludeNodesByField[n.data.field]; @@ -914,8 +914,8 @@ function GraphWorkspace(options) { const nodesByField = {}; const excludeNodesByField = {}; - //Add any blacklisted nodes to exclusion list - const avoidNodes = this.blacklistedNodes; + //Add any blocklisted nodes to exclusion list + const avoidNodes = this.blocklistedNodes; for (let i = 0; i < avoidNodes.length; i++) { const n = avoidNodes[i]; let arr = excludeNodesByField[n.data.field]; @@ -1320,12 +1320,12 @@ function GraphWorkspace(options) { allExistingNodes.forEach((existingNode) => { addTermToFieldList(excludeNodesByField, existingNode.data.field, existingNode.data.term); }); - const blacklistedNodes = self.blacklistedNodes; - blacklistedNodes.forEach((blacklistedNode) => { + const blocklistedNodes = self.blocklistedNodes; + blocklistedNodes.forEach((blocklistedNode) => { addTermToFieldList( excludeNodesByField, - blacklistedNode.data.field, - blacklistedNode.data.term + blocklistedNode.data.field, + blocklistedNode.data.term ); }); diff --git a/x-pack/plugins/graph/public/angular/graph_client_workspace.test.js b/x-pack/plugins/graph/public/angular/graph_client_workspace.test.js index fe6a782373eb2..65766cbefaad3 100644 --- a/x-pack/plugins/graph/public/angular/graph_client_workspace.test.js +++ b/x-pack/plugins/graph/public/angular/graph_client_workspace.test.js @@ -82,7 +82,7 @@ describe('graphui-workspace', function () { expect(workspace.nodes.length).toEqual(2); expect(workspace.edges.length).toEqual(1); expect(workspace.selectedNodes.length).toEqual(0); - expect(workspace.blacklistedNodes.length).toEqual(0); + expect(workspace.blocklistedNodes.length).toEqual(0); const nodeA = workspace.getNode(workspace.makeNodeId('field1', 'a')); expect(typeof nodeA).toBe('object'); @@ -124,7 +124,7 @@ describe('graphui-workspace', function () { expect(workspace.nodes.length).toEqual(2); expect(workspace.edges.length).toEqual(1); expect(workspace.selectedNodes.length).toEqual(0); - expect(workspace.blacklistedNodes.length).toEqual(0); + expect(workspace.blocklistedNodes.length).toEqual(0); mockedResult = { vertices: [ diff --git a/x-pack/plugins/graph/public/angular/templates/index.html b/x-pack/plugins/graph/public/angular/templates/index.html index 939d92518e271..50385008d7b2b 100644 --- a/x-pack/plugins/graph/public/angular/templates/index.html +++ b/x-pack/plugins/graph/public/angular/templates/index.html @@ -124,7 +124,7 @@ diff --git a/x-pack/plugins/graph/public/app.js b/x-pack/plugins/graph/public/app.js index 08b13e9d5c541..fd2b96e0570f6 100644 --- a/x-pack/plugins/graph/public/app.js +++ b/x-pack/plugins/graph/public/app.js @@ -562,8 +562,8 @@ export function initGraphApp(angularModule, deps) { run: () => { const settingsObservable = asAngularSyncedObservable( () => ({ - blacklistedNodes: $scope.workspace ? [...$scope.workspace.blacklistedNodes] : undefined, - unblacklistNode: $scope.workspace ? $scope.workspace.unblacklist : undefined, + blocklistedNodes: $scope.workspace ? [...$scope.workspace.blocklistedNodes] : undefined, + unblocklistNode: $scope.workspace ? $scope.workspace.unblocklist : undefined, canEditDrillDownUrls: canEditDrillDownUrls, }), $scope.$digest.bind($scope) diff --git a/x-pack/plugins/graph/public/components/settings/blacklist_form.tsx b/x-pack/plugins/graph/public/components/settings/blocklist_form.tsx similarity index 72% rename from x-pack/plugins/graph/public/components/settings/blacklist_form.tsx rename to x-pack/plugins/graph/public/components/settings/blocklist_form.tsx index 68cdcc1fbb7b1..29ab7611fcee8 100644 --- a/x-pack/plugins/graph/public/components/settings/blacklist_form.tsx +++ b/x-pack/plugins/graph/public/components/settings/blocklist_form.tsx @@ -20,16 +20,16 @@ import { SettingsProps } from './settings'; import { LegacyIcon } from '../legacy_icon'; import { useListKeys } from './use_list_keys'; -export function BlacklistForm({ - blacklistedNodes, - unblacklistNode, -}: Pick) { - const getListKey = useListKeys(blacklistedNodes || []); +export function BlocklistForm({ + blocklistedNodes, + unblocklistNode, +}: Pick) { + const getListKey = useListKeys(blocklistedNodes || []); return ( <> - {blacklistedNodes && blacklistedNodes.length > 0 ? ( + {blocklistedNodes && blocklistedNodes.length > 0 ? ( - {i18n.translate('xpack.graph.settings.blacklist.blacklistHelpText', { + {i18n.translate('xpack.graph.settings.blocklist.blocklistHelpText', { defaultMessage: 'These terms are not allowed in the graph.', })} @@ -37,7 +37,7 @@ export function BlacklistForm({ }} /> @@ -45,25 +45,25 @@ export function BlacklistForm({ /> )} - {blacklistedNodes && unblacklistNode && blacklistedNodes.length > 0 && ( + {blocklistedNodes && unblocklistNode && blocklistedNodes.length > 0 && ( <> - {blacklistedNodes.map((node) => ( + {blocklistedNodes.map((node) => ( } key={getListKey(node)} label={node.label} extraAction={{ iconType: 'trash', - 'aria-label': i18n.translate('xpack.graph.blacklist.removeButtonAriaLabel', { + 'aria-label': i18n.translate('xpack.graph.blocklist.removeButtonAriaLabel', { defaultMessage: 'Delete', }), - title: i18n.translate('xpack.graph.blacklist.removeButtonAriaLabel', { + title: i18n.translate('xpack.graph.blocklist.removeButtonAriaLabel', { defaultMessage: 'Delete', }), color: 'danger', onClick: () => { - unblacklistNode(node); + unblocklistNode(node); }, }} /> @@ -71,18 +71,18 @@ export function BlacklistForm({ { - blacklistedNodes.forEach((node) => { - unblacklistNode(node); + blocklistedNodes.forEach((node) => { + unblocklistNode(node); }); }} > - {i18n.translate('xpack.graph.settings.blacklist.clearButtonLabel', { + {i18n.translate('xpack.graph.settings.blocklist.clearButtonLabel', { defaultMessage: 'Delete all', })} diff --git a/x-pack/plugins/graph/public/components/settings/settings.test.tsx b/x-pack/plugins/graph/public/components/settings/settings.test.tsx index 1efaead002b52..7d13249288d53 100644 --- a/x-pack/plugins/graph/public/components/settings/settings.test.tsx +++ b/x-pack/plugins/graph/public/components/settings/settings.test.tsx @@ -46,7 +46,7 @@ describe('settings', () => { }; const angularProps: jest.Mocked = { - blacklistedNodes: [ + blocklistedNodes: [ { x: 0, y: 0, @@ -57,7 +57,7 @@ describe('settings', () => { field: 'A', term: '1', }, - label: 'blacklisted node 1', + label: 'blocklisted node 1', icon: { class: 'test', code: '1', @@ -74,7 +74,7 @@ describe('settings', () => { field: 'A', term: '1', }, - label: 'blacklisted node 2', + label: 'blocklisted node 2', icon: { class: 'test', code: '1', @@ -82,7 +82,7 @@ describe('settings', () => { }, }, ], - unblacklistNode: jest.fn(), + unblocklistNode: jest.fn(), canEditDrillDownUrls: true, }; @@ -201,15 +201,15 @@ describe('settings', () => { }); }); - describe('blacklist', () => { + describe('blocklist', () => { beforeEach(() => { toTab('Block list'); }); - it('should switch tab to blacklist', () => { + it('should switch tab to blocklist', () => { expect(instance.find(EuiListGroupItem).map((item) => item.prop('label'))).toEqual([ - 'blacklisted node 1', - 'blacklisted node 2', + 'blocklisted node 1', + 'blocklisted node 2', ]); }); @@ -217,7 +217,7 @@ describe('settings', () => { act(() => { subject.next({ ...angularProps, - blacklistedNodes: [ + blocklistedNodes: [ { x: 0, y: 0, @@ -228,7 +228,7 @@ describe('settings', () => { field: 'A', term: '1', }, - label: 'blacklisted node 3', + label: 'blocklisted node 3', icon: { class: 'test', code: '1', @@ -242,21 +242,21 @@ describe('settings', () => { instance.update(); expect(instance.find(EuiListGroupItem).map((item) => item.prop('label'))).toEqual([ - 'blacklisted node 3', + 'blocklisted node 3', ]); }); it('should delete node', () => { instance.find(EuiListGroupItem).at(0).prop('extraAction')!.onClick!({} as any); - expect(angularProps.unblacklistNode).toHaveBeenCalledWith(angularProps.blacklistedNodes![0]); + expect(angularProps.unblocklistNode).toHaveBeenCalledWith(angularProps.blocklistedNodes![0]); }); it('should delete all nodes', () => { - instance.find('[data-test-subj="graphUnblacklistAll"]').find(EuiButton).simulate('click'); + instance.find('[data-test-subj="graphUnblocklistAll"]').find(EuiButton).simulate('click'); - expect(angularProps.unblacklistNode).toHaveBeenCalledWith(angularProps.blacklistedNodes![0]); - expect(angularProps.unblacklistNode).toHaveBeenCalledWith(angularProps.blacklistedNodes![1]); + expect(angularProps.unblocklistNode).toHaveBeenCalledWith(angularProps.blocklistedNodes![0]); + expect(angularProps.unblocklistNode).toHaveBeenCalledWith(angularProps.blocklistedNodes![1]); }); }); diff --git a/x-pack/plugins/graph/public/components/settings/settings.tsx b/x-pack/plugins/graph/public/components/settings/settings.tsx index 3baf6b6a0a2e3..3a9ea6e96859b 100644 --- a/x-pack/plugins/graph/public/components/settings/settings.tsx +++ b/x-pack/plugins/graph/public/components/settings/settings.tsx @@ -11,7 +11,7 @@ import * as Rx from 'rxjs'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { AdvancedSettingsForm } from './advanced_settings_form'; -import { BlacklistForm } from './blacklist_form'; +import { BlocklistForm } from './blocklist_form'; import { UrlTemplateList } from './url_template_list'; import { WorkspaceNode, AdvancedSettings, UrlTemplate, WorkspaceField } from '../../types'; import { @@ -33,9 +33,9 @@ const tabs = [ component: AdvancedSettingsForm, }, { - id: 'blacklist', - title: i18n.translate('xpack.graph.settings.blacklistTitle', { defaultMessage: 'Block list' }), - component: BlacklistForm, + id: 'blocklist', + title: i18n.translate('xpack.graph.settings.blocklistTitle', { defaultMessage: 'Block list' }), + component: BlocklistForm, }, { id: 'drillDowns', @@ -51,8 +51,8 @@ const tabs = [ * to catch update outside updates */ export interface AngularProps { - blacklistedNodes: WorkspaceNode[]; - unblacklistNode: (node: WorkspaceNode) => void; + blocklistedNodes: WorkspaceNode[]; + unblocklistNode: (node: WorkspaceNode) => void; canEditDrillDownUrls: boolean; } diff --git a/x-pack/plugins/graph/public/services/persistence/deserialize.test.ts b/x-pack/plugins/graph/public/services/persistence/deserialize.test.ts index 3dda41fcdbdb6..e9f116b79f990 100644 --- a/x-pack/plugins/graph/public/services/persistence/deserialize.test.ts +++ b/x-pack/plugins/graph/public/services/persistence/deserialize.test.ts @@ -26,7 +26,7 @@ describe('deserialize', () => { { color: 'black', name: 'field1', selected: true, iconClass: 'a' }, { color: 'black', name: 'field2', selected: true, iconClass: 'b' }, ], - blacklist: [ + blocklist: [ { color: 'black', label: 'Z', @@ -192,7 +192,7 @@ describe('deserialize', () => { it('should deserialize nodes and edges', () => { callSavedWorkspaceToAppState(); - expect(workspace.blacklistedNodes.length).toEqual(1); + expect(workspace.blocklistedNodes.length).toEqual(1); expect(workspace.nodes.length).toEqual(5); expect(workspace.edges.length).toEqual(2); diff --git a/x-pack/plugins/graph/public/services/persistence/deserialize.ts b/x-pack/plugins/graph/public/services/persistence/deserialize.ts index 6fd720a60edc0..324bf10cdd99c 100644 --- a/x-pack/plugins/graph/public/services/persistence/deserialize.ts +++ b/x-pack/plugins/graph/public/services/persistence/deserialize.ts @@ -128,11 +128,11 @@ function getFieldsWithWorkspaceSettings( return allFields; } -function getBlacklistedNodes( +function getBlocklistedNodes( serializedWorkspaceState: SerializedWorkspaceState, allFields: WorkspaceField[] ) { - return serializedWorkspaceState.blacklist.map((serializedNode) => { + return serializedWorkspaceState.blocklist.map((serializedNode) => { const currentField = allFields.find((field) => field.name === serializedNode.field)!; return { x: 0, @@ -235,9 +235,9 @@ export function savedWorkspaceToAppState( workspaceInstance.mergeGraph(graph); resolveGroups(persistedWorkspaceState.vertices, workspaceInstance); - // ================== blacklist ============================= - const blacklistedNodes = getBlacklistedNodes(persistedWorkspaceState, allFields); - workspaceInstance.blacklistedNodes.push(...blacklistedNodes); + // ================== blocklist ============================= + const blocklistedNodes = getBlocklistedNodes(persistedWorkspaceState, allFields); + workspaceInstance.blocklistedNodes.push(...blocklistedNodes); return { urlTemplates, diff --git a/x-pack/plugins/graph/public/services/persistence/serialize.test.ts b/x-pack/plugins/graph/public/services/persistence/serialize.test.ts index a3942eccfdac3..0c9de0418a738 100644 --- a/x-pack/plugins/graph/public/services/persistence/serialize.test.ts +++ b/x-pack/plugins/graph/public/services/persistence/serialize.test.ts @@ -118,7 +118,7 @@ describe('serialize', () => { parent: null, }, ], - blacklistedNodes: [ + blocklistedNodes: [ { color: 'black', data: { field: 'field1', term: 'Z' }, @@ -165,7 +165,7 @@ describe('serialize', () => { const workspaceState = JSON.parse(savedWorkspace.wsState); expect(workspaceState).toMatchInlineSnapshot(` Object { - "blacklist": Array [ + "blocklist": Array [ Object { "color": "black", "field": "field1", diff --git a/x-pack/plugins/graph/public/services/persistence/serialize.ts b/x-pack/plugins/graph/public/services/persistence/serialize.ts index 6cbebc995d84a..a3a76a8a08eba 100644 --- a/x-pack/plugins/graph/public/services/persistence/serialize.ts +++ b/x-pack/plugins/graph/public/services/persistence/serialize.ts @@ -96,8 +96,8 @@ export function appStateToSavedWorkspace( }, canSaveData: boolean ) { - const blacklist: SerializedNode[] = canSaveData - ? workspace.blacklistedNodes.map((node) => serializeNode(node)) + const blocklist: SerializedNode[] = canSaveData + ? workspace.blocklistedNodes.map((node) => serializeNode(node)) : []; const vertices: SerializedNode[] = canSaveData ? workspace.nodes.map((node) => serializeNode(node, workspace.nodes)) @@ -111,7 +111,7 @@ export function appStateToSavedWorkspace( const persistedWorkspaceState: SerializedWorkspaceState = { indexPattern: selectedIndex.title, selectedFields: selectedFields.map(serializeField), - blacklist, + blocklist, vertices, links, urlTemplates: mappedUrlTemplates, diff --git a/x-pack/plugins/graph/public/state_management/mocks.ts b/x-pack/plugins/graph/public/state_management/mocks.ts index 5a0269d691de2..d32bc9a175a47 100644 --- a/x-pack/plugins/graph/public/state_management/mocks.ts +++ b/x-pack/plugins/graph/public/state_management/mocks.ts @@ -46,7 +46,7 @@ export function createMockGraphStore({ nodes: [], edges: [], options: {}, - blacklistedNodes: [], + blocklistedNodes: [], } as unknown) as Workspace; const savedWorkspace = ({ diff --git a/x-pack/plugins/graph/public/state_management/persistence.ts b/x-pack/plugins/graph/public/state_management/persistence.ts index cd2c6680c1fd2..cf6566f0c5f86 100644 --- a/x-pack/plugins/graph/public/state_management/persistence.ts +++ b/x-pack/plugins/graph/public/state_management/persistence.ts @@ -198,7 +198,7 @@ function showModal( openSaveModal({ savePolicy: deps.savePolicy, - hasData: workspace.nodes.length > 0 || workspace.blacklistedNodes.length > 0, + hasData: workspace.nodes.length > 0 || workspace.blocklistedNodes.length > 0, workspace: savedWorkspace, showSaveModal: deps.showSaveModal, saveWorkspace: saveWorkspaceHandler, diff --git a/x-pack/plugins/graph/public/types/persistence.ts b/x-pack/plugins/graph/public/types/persistence.ts index 6847199d5878c..8e7e9c7e8878e 100644 --- a/x-pack/plugins/graph/public/types/persistence.ts +++ b/x-pack/plugins/graph/public/types/persistence.ts @@ -33,7 +33,7 @@ export interface GraphWorkspaceSavedObject { export interface SerializedWorkspaceState { indexPattern: string; selectedFields: SerializedField[]; - blacklist: SerializedNode[]; + blocklist: SerializedNode[]; vertices: SerializedNode[]; links: SerializedEdge[]; urlTemplates: SerializedUrlTemplate[]; diff --git a/x-pack/plugins/graph/public/types/workspace_state.ts b/x-pack/plugins/graph/public/types/workspace_state.ts index 8c4178eda890f..b5ee48311ddc8 100644 --- a/x-pack/plugins/graph/public/types/workspace_state.ts +++ b/x-pack/plugins/graph/public/types/workspace_state.ts @@ -63,7 +63,7 @@ export interface Workspace { nodesMap: Record; nodes: WorkspaceNode[]; edges: WorkspaceEdge[]; - blacklistedNodes: WorkspaceNode[]; + blocklistedNodes: WorkspaceNode[]; getQuery(startNodes?: WorkspaceNode[], loose?: boolean): JsonObject; getSelectedOrAllNodes(): WorkspaceNode[]; diff --git a/x-pack/plugins/graph/server/sample_data/ecommerce.ts b/x-pack/plugins/graph/server/sample_data/ecommerce.ts index 7543e9471f05c..b9b4e063cb28f 100644 --- a/x-pack/plugins/graph/server/sample_data/ecommerce.ts +++ b/x-pack/plugins/graph/server/sample_data/ecommerce.ts @@ -37,7 +37,7 @@ const wsState: any = { iconClass: 'fa-heart', }, ], - blacklist: [ + blocklist: [ { x: 491.3880229084531, y: 572.375603969653, diff --git a/x-pack/plugins/graph/server/sample_data/flights.ts b/x-pack/plugins/graph/server/sample_data/flights.ts index bca1d0d093a8e..209b7108266cf 100644 --- a/x-pack/plugins/graph/server/sample_data/flights.ts +++ b/x-pack/plugins/graph/server/sample_data/flights.ts @@ -37,7 +37,7 @@ const wsState: any = { iconClass: 'fa-cube', }, ], - blacklist: [], + blocklist: [], vertices: [ { x: 324.55695700802687, diff --git a/x-pack/plugins/graph/server/sample_data/logs.ts b/x-pack/plugins/graph/server/sample_data/logs.ts index 5ca810b397cd2..c3cc2ecd2fc65 100644 --- a/x-pack/plugins/graph/server/sample_data/logs.ts +++ b/x-pack/plugins/graph/server/sample_data/logs.ts @@ -45,7 +45,7 @@ const wsState: any = { iconClass: 'fa-key', }, ], - blacklist: [ + blocklist: [ { x: 349.9814471314239, y: 274.1259761174194, diff --git a/x-pack/plugins/graph/server/saved_objects/migrations.ts b/x-pack/plugins/graph/server/saved_objects/migrations.ts index beb31d548c670..34cd59e2220e9 100644 --- a/x-pack/plugins/graph/server/saved_objects/migrations.ts +++ b/x-pack/plugins/graph/server/saved_objects/migrations.ts @@ -37,4 +37,23 @@ export const graphMigrations = { }); return doc; }, + '7.10.0': (doc: SavedObjectUnsanitizedDoc) => { + const wsState = get(doc, 'attributes.wsState'); + if (typeof wsState !== 'string') { + return doc; + } + let state; + try { + state = JSON.parse(JSON.parse(wsState)); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + return doc; + } + if (state.blacklist) { + state.blocklist = state.blacklist; + delete state.blacklist; + } + doc.attributes.wsState = JSON.stringify(JSON.stringify(state)); + return doc; + }, }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2787acb5390b9..a9a9e81f04b72 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6185,8 +6185,8 @@ "xpack.graph.bar.pickSourceLabel": "データソースを選択", "xpack.graph.bar.pickSourceTooltip": "グラフの関係性を開始するデータソースを選択します。", "xpack.graph.bar.searchFieldPlaceholder": "データを検索してグラフに追加", - "xpack.graph.blacklist.noEntriesDescription": "ブロックされた用語がありません。頂点を選択して、右側のコントロールパネルの {stopSign} をクリックしてブロックします。ブロックされた用語に一致するドキュメントは今後表示されず、関係性が非表示になります。", - "xpack.graph.blacklist.removeButtonAriaLabel": "削除", + "xpack.graph.blocklist.noEntriesDescription": "ブロックされた用語がありません。頂点を選択して、右側のコントロールパネルの {stopSign} をクリックしてブロックします。ブロックされた用語に一致するドキュメントは今後表示されず、関係性が非表示になります。", + "xpack.graph.blocklist.removeButtonAriaLabel": "削除", "xpack.graph.clearWorkspace.confirmButtonLabel": "データソースを変更", "xpack.graph.clearWorkspace.confirmText": "データソースを変更すると、現在のフィールドと頂点がリセットされます。", "xpack.graph.clearWorkspace.modalTitle": "保存されていない変更", @@ -6322,9 +6322,9 @@ "xpack.graph.settings.advancedSettings.timeoutInputLabel": "タイムアウト (ms)", "xpack.graph.settings.advancedSettings.timeoutUnit": "ms", "xpack.graph.settings.advancedSettingsTitle": "高度な設定", - "xpack.graph.settings.blacklist.blacklistHelpText": "これらの用語は現在ワークスペースに再度表示されないようブラックリストに登録されています", - "xpack.graph.settings.blacklist.clearButtonLabel": "消去", - "xpack.graph.settings.blacklistTitle": "ブラックリスト", + "xpack.graph.settings.blocklist.blocklistHelpText": "これらの用語は現在ワークスペースに再度表示されないようブラックリストに登録されています", + "xpack.graph.settings.blocklist.clearButtonLabel": "消去", + "xpack.graph.settings.blocklistTitle": "ブラックリスト", "xpack.graph.settings.closeLabel": "閉じる", "xpack.graph.settings.drillDowns.cancelButtonLabel": "キャンセル", "xpack.graph.settings.drillDowns.defaultUrlTemplateTitle": "生ドキュメント", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b42c4dbb28569..95eb93d9e37bb 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6189,8 +6189,8 @@ "xpack.graph.bar.pickSourceLabel": "选择数据源", "xpack.graph.bar.pickSourceTooltip": "选择数据源以开始绘制关系图。", "xpack.graph.bar.searchFieldPlaceholder": "搜索数据并将其添加到图表", - "xpack.graph.blacklist.noEntriesDescription": "您没有任何已阻止字词。选择顶点并单击右侧控制面板上的 {stopSign} 以阻止它们。匹配已阻止字词的文档将不再被浏览,与它们的关系将隐藏。", - "xpack.graph.blacklist.removeButtonAriaLabel": "删除", + "xpack.graph.blocklist.noEntriesDescription": "您没有任何已阻止字词。选择顶点并单击右侧控制面板上的 {stopSign} 以阻止它们。匹配已阻止字词的文档将不再被浏览,与它们的关系将隐藏。", + "xpack.graph.blocklist.removeButtonAriaLabel": "删除", "xpack.graph.clearWorkspace.confirmButtonLabel": "更改数据源", "xpack.graph.clearWorkspace.confirmText": "如果更改数据源,您当前的字段和顶点将会重置。", "xpack.graph.clearWorkspace.modalTitle": "未保存更改", @@ -6326,9 +6326,9 @@ "xpack.graph.settings.advancedSettings.timeoutInputLabel": "超时 (ms)", "xpack.graph.settings.advancedSettings.timeoutUnit": "ms", "xpack.graph.settings.advancedSettingsTitle": "高级设置", - "xpack.graph.settings.blacklist.blacklistHelpText": "这些字词当前已列入黑名单,不允许重新显示在工作空间中。", - "xpack.graph.settings.blacklist.clearButtonLabel": "清除", - "xpack.graph.settings.blacklistTitle": "黑名单", + "xpack.graph.settings.blocklist.blocklistHelpText": "这些字词当前已列入黑名单,不允许重新显示在工作空间中。", + "xpack.graph.settings.blocklist.clearButtonLabel": "清除", + "xpack.graph.settings.blocklistTitle": "黑名单", "xpack.graph.settings.closeLabel": "关闭", "xpack.graph.settings.drillDowns.cancelButtonLabel": "取消", "xpack.graph.settings.drillDowns.defaultUrlTemplateTitle": "原始文档", From 2c9dac2cef3665d9bf6d5ecb0b0aeda1b5ac21f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 14 Jul 2020 17:13:58 +0100 Subject: [PATCH 10/82] [APM] Respect default time range defined in Kibana Advanced Settings (#71464) * using kibana settings on date picker * fixing unit tests and use date picker component to update the query params * fixing translations --- .../__test__/__snapshots__/List.test.tsx.snap | 48 +++---- .../app/Home/__snapshots__/Home.test.tsx.snap | 6 + .../app/Main/UpdateBreadcrumbs.test.tsx | 65 ++++++++-- .../UpdateBreadcrumbs.test.tsx.snap | 102 --------------- .../ServiceOverview.test.tsx.snap | 4 +- .../DatePicker/__test__/DatePicker.test.tsx | 58 +++++++-- .../components/shared/DatePicker/index.tsx | 118 +++++++++--------- .../components/shared/DatePicker/typings.ts | 21 ++++ .../DiscoverLinks.integration.test.tsx | 12 +- .../MachineLearningLinks/MLJobLink.test.tsx | 10 +- .../MachineLearningLinks/MLLink.test.tsx | 3 +- .../shared/Links/apm/APMLink.test.tsx | 13 +- .../components/shared/Links/apm/APMLink.tsx | 2 - .../components/shared/Links/rison_helpers.ts | 19 +-- .../__test__/sections.test.ts | 24 +++- .../Timeline/Marker/ErrorMarker.test.tsx | 4 +- .../ApmPluginContext/MockApmPluginContext.tsx | 27 ++++ .../__tests__/UrlParamsContext.test.tsx | 6 - .../context/UrlParamsContext/constants.ts | 6 - .../UrlParamsContext/resolveUrlParams.ts | 13 +- .../translations/translations/ja-JP.json | 8 -- .../translations/translations/zh-CN.json | 8 -- 22 files changed, 309 insertions(+), 268 deletions(-) delete mode 100644 x-pack/plugins/apm/public/components/app/Main/__snapshots__/UpdateBreadcrumbs.test.tsx.snap create mode 100644 x-pack/plugins/apm/public/components/shared/DatePicker/typings.ts diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap index a86f7fdf41f4f..0589fce727115 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap @@ -786,11 +786,11 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > a0ce2 @@ -831,11 +831,11 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > @@ -878,13 +878,13 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > List should render with data 1`] = ` > f3ac9 @@ -1065,11 +1065,11 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > @@ -1112,13 +1112,13 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > List should render with data 1`] = ` > e9086 @@ -1299,11 +1299,11 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > @@ -1346,13 +1346,13 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > List should render with data 1`] = ` > 8673d @@ -1533,11 +1533,11 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > @@ -1580,13 +1580,13 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > ); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs.mock.calls[0][0]).toMatchSnapshot(); } describe('UpdateBreadcrumbs', () => { @@ -58,36 +57,88 @@ describe('UpdateBreadcrumbs', () => { }); it('Homepage', () => { - expectBreadcrumbToMatchSnapshot('/'); + mountBreadcrumb('/'); expect(window.document.title).toMatchInlineSnapshot(`"APM"`); }); it('/services/:serviceName/errors/:groupId', () => { - expectBreadcrumbToMatchSnapshot('/services/opbeans-node/errors/myGroupId'); + mountBreadcrumb( + '/services/opbeans-node/errors/myGroupId', + 'rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0' + ); + const breadcrumbs = setBreadcrumbs.mock.calls[0][0]; + expect(breadcrumbs).toEqual([ + { + text: 'APM', + href: + '#/?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0', + }, + { + text: 'Services', + href: + '#/services?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0', + }, + { + text: 'opbeans-node', + href: + '#/services/opbeans-node?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0', + }, + { + text: 'Errors', + href: + '#/services/opbeans-node/errors?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0', + }, + { text: 'myGroupId', href: undefined }, + ]); expect(window.document.title).toMatchInlineSnapshot( `"myGroupId | Errors | opbeans-node | Services | APM"` ); }); it('/services/:serviceName/errors', () => { - expectBreadcrumbToMatchSnapshot('/services/opbeans-node/errors'); + mountBreadcrumb('/services/opbeans-node/errors'); + const breadcrumbs = setBreadcrumbs.mock.calls[0][0]; + expect(breadcrumbs).toEqual([ + { text: 'APM', href: '#/?kuery=myKuery' }, + { text: 'Services', href: '#/services?kuery=myKuery' }, + { text: 'opbeans-node', href: '#/services/opbeans-node?kuery=myKuery' }, + { text: 'Errors', href: undefined }, + ]); expect(window.document.title).toMatchInlineSnapshot( `"Errors | opbeans-node | Services | APM"` ); }); it('/services/:serviceName/transactions', () => { - expectBreadcrumbToMatchSnapshot('/services/opbeans-node/transactions'); + mountBreadcrumb('/services/opbeans-node/transactions'); + const breadcrumbs = setBreadcrumbs.mock.calls[0][0]; + expect(breadcrumbs).toEqual([ + { text: 'APM', href: '#/?kuery=myKuery' }, + { text: 'Services', href: '#/services?kuery=myKuery' }, + { text: 'opbeans-node', href: '#/services/opbeans-node?kuery=myKuery' }, + { text: 'Transactions', href: undefined }, + ]); expect(window.document.title).toMatchInlineSnapshot( `"Transactions | opbeans-node | Services | APM"` ); }); it('/services/:serviceName/transactions/view?transactionName=my-transaction-name', () => { - expectBreadcrumbToMatchSnapshot( + mountBreadcrumb( '/services/opbeans-node/transactions/view', 'transactionName=my-transaction-name' ); + const breadcrumbs = setBreadcrumbs.mock.calls[0][0]; + expect(breadcrumbs).toEqual([ + { text: 'APM', href: '#/?kuery=myKuery' }, + { text: 'Services', href: '#/services?kuery=myKuery' }, + { text: 'opbeans-node', href: '#/services/opbeans-node?kuery=myKuery' }, + { + text: 'Transactions', + href: '#/services/opbeans-node/transactions?kuery=myKuery', + }, + { text: 'my-transaction-name', href: undefined }, + ]); expect(window.document.title).toMatchInlineSnapshot( `"my-transaction-name | Transactions | opbeans-node | Services | APM"` ); diff --git a/x-pack/plugins/apm/public/components/app/Main/__snapshots__/UpdateBreadcrumbs.test.tsx.snap b/x-pack/plugins/apm/public/components/app/Main/__snapshots__/UpdateBreadcrumbs.test.tsx.snap deleted file mode 100644 index e7f6cba59318a..0000000000000 --- a/x-pack/plugins/apm/public/components/app/Main/__snapshots__/UpdateBreadcrumbs.test.tsx.snap +++ /dev/null @@ -1,102 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`UpdateBreadcrumbs /services/:serviceName/errors 1`] = ` -Array [ - Object { - "href": "#/?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", - "text": "APM", - }, - Object { - "href": "#/services?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", - "text": "Services", - }, - Object { - "href": "#/services/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", - "text": "opbeans-node", - }, - Object { - "href": undefined, - "text": "Errors", - }, -] -`; - -exports[`UpdateBreadcrumbs /services/:serviceName/errors/:groupId 1`] = ` -Array [ - Object { - "href": "#/?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", - "text": "APM", - }, - Object { - "href": "#/services?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", - "text": "Services", - }, - Object { - "href": "#/services/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", - "text": "opbeans-node", - }, - Object { - "href": "#/services/opbeans-node/errors?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", - "text": "Errors", - }, - Object { - "href": undefined, - "text": "myGroupId", - }, -] -`; - -exports[`UpdateBreadcrumbs /services/:serviceName/transactions 1`] = ` -Array [ - Object { - "href": "#/?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", - "text": "APM", - }, - Object { - "href": "#/services?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", - "text": "Services", - }, - Object { - "href": "#/services/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", - "text": "opbeans-node", - }, - Object { - "href": undefined, - "text": "Transactions", - }, -] -`; - -exports[`UpdateBreadcrumbs /services/:serviceName/transactions/view?transactionName=my-transaction-name 1`] = ` -Array [ - Object { - "href": "#/?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", - "text": "APM", - }, - Object { - "href": "#/services?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", - "text": "Services", - }, - Object { - "href": "#/services/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", - "text": "opbeans-node", - }, - Object { - "href": "#/services/opbeans-node/transactions?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", - "text": "Transactions", - }, - Object { - "href": undefined, - "text": "my-transaction-name", - }, -] -`; - -exports[`UpdateBreadcrumbs Homepage 1`] = ` -Array [ - Object { - "href": undefined, - "text": "APM", - }, -] -`; diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap index 241ba8c244496..e46da26f7dcb0 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap @@ -157,7 +157,7 @@ NodeList [ > My Go Service @@ -263,7 +263,7 @@ NodeList [ > My Python Service diff --git a/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx b/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx index 639277a79ac9a..215e97aebf646 100644 --- a/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx @@ -17,6 +17,7 @@ import { mount } from 'enzyme'; import { EuiSuperDatePicker } from '@elastic/eui'; import { MemoryRouter } from 'react-router-dom'; import { wait } from '@testing-library/react'; +import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; const mockHistoryPush = jest.spyOn(history, 'push'); const mockRefreshTimeRange = jest.fn(); @@ -35,13 +36,15 @@ const MockUrlParamsProvider: React.FC<{ function mountDatePicker(params?: IUrlParams) { return mount( - - - - - - - + + + + + + + + + ); } @@ -58,6 +61,41 @@ describe('DatePicker', () => { jest.clearAllMocks(); }); + it('should set default query params in the URL', () => { + mountDatePicker(); + expect(mockHistoryPush).toHaveBeenCalledTimes(1); + expect(mockHistoryPush).toHaveBeenCalledWith( + expect.objectContaining({ + search: + 'rangeFrom=now-15m&rangeTo=now&refreshPaused=false&refreshInterval=10000', + }) + ); + }); + + it('should add missing default value', () => { + mountDatePicker({ + rangeTo: 'now', + refreshInterval: 5000, + }); + expect(mockHistoryPush).toHaveBeenCalledTimes(1); + expect(mockHistoryPush).toHaveBeenCalledWith( + expect.objectContaining({ + search: + 'rangeFrom=now-15m&rangeTo=now&refreshInterval=5000&refreshPaused=false', + }) + ); + }); + + it('should not set default query params in the URL when values already defined', () => { + mountDatePicker({ + rangeFrom: 'now-1d', + rangeTo: 'now', + refreshPaused: false, + refreshInterval: 5000, + }); + expect(mockHistoryPush).toHaveBeenCalledTimes(0); + }); + it('should update the URL when the date range changes', () => { const datePicker = mountDatePicker(); datePicker.find(EuiSuperDatePicker).props().onTimeChange({ @@ -66,9 +104,11 @@ describe('DatePicker', () => { isInvalid: false, isQuickSelection: true, }); - expect(mockHistoryPush).toHaveBeenCalledWith( + expect(mockHistoryPush).toHaveBeenCalledTimes(2); + expect(mockHistoryPush).toHaveBeenLastCalledWith( expect.objectContaining({ - search: 'rangeFrom=updated-start&rangeTo=updated-end', + search: + 'rangeFrom=updated-start&rangeTo=updated-end&refreshInterval=5000&refreshPaused=false', }) ); }); diff --git a/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx b/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx index 4391e4a5b8952..5201d80de5a12 100644 --- a/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx @@ -5,75 +5,61 @@ */ import { EuiSuperDatePicker } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import React from 'react'; +import { isEmpty, isEqual, pickBy } from 'lodash'; import { fromQuery, toQuery } from '../Links/url_helpers'; import { history } from '../../../utils/history'; import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { clearCache } from '../../../services/rest/callApi'; +import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { UI_SETTINGS } from '../../../../../../../src/plugins/data/common'; +import { + TimePickerQuickRange, + TimePickerTimeDefaults, + TimePickerRefreshInterval, +} from './typings'; + +function removeUndefinedAndEmptyProps(obj: T): Partial { + return pickBy(obj, (value) => value !== undefined && !isEmpty(String(value))); +} export function DatePicker() { const location = useLocation(); + const { core } = useApmPluginContext(); + + const timePickerQuickRanges = core.uiSettings.get( + UI_SETTINGS.TIMEPICKER_QUICK_RANGES + ); + + const timePickerTimeDefaults = core.uiSettings.get( + UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS + ); + + const timePickerRefreshIntervalDefaults = core.uiSettings.get< + TimePickerRefreshInterval + >(UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS); + + const DEFAULT_VALUES = { + rangeFrom: timePickerTimeDefaults.from, + rangeTo: timePickerTimeDefaults.to, + refreshPaused: timePickerRefreshIntervalDefaults.pause, + /* + * Must be replaced by timePickerRefreshIntervalDefaults.value when this issue is fixed. + * https://github.com/elastic/kibana/issues/70562 + */ + refreshInterval: 10000, + }; + + const commonlyUsedRanges = timePickerQuickRanges.map( + ({ from, to, display }) => ({ + start: from, + end: to, + label: display, + }) + ); + const { urlParams, refreshTimeRange } = useUrlParams(); - const commonlyUsedRanges = [ - { - start: 'now-15m', - end: 'now', - label: i18n.translate('xpack.apm.datePicker.last15MinutesLabel', { - defaultMessage: 'Last 15 minutes', - }), - }, - { - start: 'now-30m', - end: 'now', - label: i18n.translate('xpack.apm.datePicker.last30MinutesLabel', { - defaultMessage: 'Last 30 minutes', - }), - }, - { - start: 'now-1h', - end: 'now', - label: i18n.translate('xpack.apm.datePicker.last1HourLabel', { - defaultMessage: 'Last 1 hour', - }), - }, - { - start: 'now-24h', - end: 'now', - label: i18n.translate('xpack.apm.datePicker.last24HoursLabel', { - defaultMessage: 'Last 24 hours', - }), - }, - { - start: 'now-7d', - end: 'now', - label: i18n.translate('xpack.apm.datePicker.last7DaysLabel', { - defaultMessage: 'Last 7 days', - }), - }, - { - start: 'now-30d', - end: 'now', - label: i18n.translate('xpack.apm.datePicker.last30DaysLabel', { - defaultMessage: 'Last 30 days', - }), - }, - { - start: 'now-90d', - end: 'now', - label: i18n.translate('xpack.apm.datePicker.last90DaysLabel', { - defaultMessage: 'Last 90 days', - }), - }, - { - start: 'now-1y', - end: 'now', - label: i18n.translate('xpack.apm.datePicker.last1YearLabel', { - defaultMessage: 'Last 1 year', - }), - }, - ]; function updateUrl(nextQuery: { rangeFrom?: string; @@ -105,6 +91,20 @@ export function DatePicker() { } const { rangeFrom, rangeTo, refreshPaused, refreshInterval } = urlParams; + const timePickerURLParams = removeUndefinedAndEmptyProps({ + rangeFrom, + rangeTo, + refreshPaused, + refreshInterval, + }); + + const nextParams = { + ...DEFAULT_VALUES, + ...timePickerURLParams, + }; + if (!isEqual(nextParams, timePickerURLParams)) { + updateUrl(nextParams); + } return ( { const href = await getRenderedHref( () => , { - search: '?rangeFrom=now/w&rangeTo=now', + search: + '?rangeFrom=now/w&rangeTo=now&refreshPaused=true&refreshInterval=0', } as Location ); @@ -45,7 +46,8 @@ describe('DiscoverLinks', () => { } as Span; const href = await getRenderedHref(() => , { - search: '?rangeFrom=now/w&rangeTo=now', + search: + '?rangeFrom=now/w&rangeTo=now&refreshPaused=true&refreshInterval=0', } as Location); expect(href).toEqual( @@ -65,7 +67,8 @@ describe('DiscoverLinks', () => { const href = await getRenderedHref( () => , { - search: '?rangeFrom=now/w&rangeTo=now', + search: + '?rangeFrom=now/w&rangeTo=now&refreshPaused=true&refreshInterval=0', } as Location ); @@ -87,7 +90,8 @@ describe('DiscoverLinks', () => { const href = await getRenderedHref( () => , { - search: '?rangeFrom=now/w&rangeTo=now', + search: + '?rangeFrom=now/w&rangeTo=now&refreshPaused=true&refreshInterval=0', } as Location ); diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx index c832d3ded6175..39082c2639a2c 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx @@ -15,7 +15,10 @@ describe('MLJobLink', () => { () => ( ), - { search: '?rangeFrom=now/w&rangeTo=now-4h' } as Location + { + search: + '?rangeFrom=now/w&rangeTo=now-4h&refreshPaused=true&refreshInterval=0', + } as Location ); expect(href).toMatchInlineSnapshot( @@ -31,7 +34,10 @@ describe('MLJobLink', () => { transactionType="request" /> ), - { search: '?rangeFrom=now/w&rangeTo=now-4h' } as Location + { + search: + '?rangeFrom=now/w&rangeTo=now-4h&refreshPaused=true&refreshInterval=0', + } as Location ); expect(href).toMatchInlineSnapshot( diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx index 840846adae019..b4187b2f797ab 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx @@ -15,7 +15,8 @@ test('MLLink produces the correct URL', async () => { ), { - search: '?rangeFrom=now-5h&rangeTo=now-2h', + search: + '?rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0', } as Location ); diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.test.tsx index d6518e76aa5e9..1e849e8865d0d 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.test.tsx @@ -13,7 +13,8 @@ test('APMLink should produce the correct URL', async () => { const href = await getRenderedHref( () => , { - search: '?rangeFrom=now-5h&rangeTo=now-2h', + search: + '?rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0', } as Location ); @@ -26,12 +27,13 @@ test('APMLink should retain current kuery value if it exists', async () => { const href = await getRenderedHref( () => , { - search: '?kuery=host.hostname~20~3A~20~22fakehostname~22', + search: + '?kuery=host.hostname~20~3A~20~22fakehostname~22&rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0', } as Location ); expect(href).toMatchInlineSnapshot( - `"#/some/path?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=host.hostname~20~3A~20~22fakehostname~22&transactionId=blah"` + `"#/some/path?kuery=host.hostname~20~3A~20~22fakehostname~22&rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0&transactionId=blah"` ); }); @@ -44,11 +46,12 @@ test('APMLink should overwrite current kuery value if new kuery value is provide /> ), { - search: '?kuery=host.hostname~20~3A~20~22fakehostname~22', + search: + '?kuery=host.hostname~20~3A~20~22fakehostname~22&rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0', } as Location ); expect(href).toMatchInlineSnapshot( - `"#/some/path?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=host.os~20~3A~20~22linux~22"` + `"#/some/path?kuery=host.os~20~3A~20~22linux~22&rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0"` ); }); diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx index 3aff241c6dee2..353f476e3f993 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx @@ -10,7 +10,6 @@ import url from 'url'; import { pick } from 'lodash'; import { useLocation } from '../../../../hooks/useLocation'; import { APMQueryParams, toQuery, fromQuery } from '../url_helpers'; -import { TIMEPICKER_DEFAULTS } from '../../../../context/UrlParamsContext/constants'; interface Props extends EuiLinkAnchorProps { path?: string; @@ -36,7 +35,6 @@ export function getAPMHref( ) { const currentQuery = toQuery(currentSearch); const nextQuery = { - ...TIMEPICKER_DEFAULTS, ...pick(currentQuery, PERSISTENT_APM_PARAMS), ...query, }; diff --git a/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts b/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts index 434bd285029ab..8b4d891dba83b 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts @@ -5,7 +5,6 @@ */ import { Location } from 'history'; -import { TIMEPICKER_DEFAULTS } from '../../../context/UrlParamsContext/constants'; import { toQuery } from './url_helpers'; export interface TimepickerRisonData { @@ -21,18 +20,20 @@ export interface TimepickerRisonData { export function getTimepickerRisonData(currentSearch: Location['search']) { const currentQuery = toQuery(currentSearch); - const nextQuery = { - ...TIMEPICKER_DEFAULTS, - ...currentQuery, - }; return { time: { - from: encodeURIComponent(nextQuery.rangeFrom), - to: encodeURIComponent(nextQuery.rangeTo), + from: currentQuery.rangeFrom + ? encodeURIComponent(currentQuery.rangeFrom) + : '', + to: currentQuery.rangeTo ? encodeURIComponent(currentQuery.rangeTo) : '', }, refreshInterval: { - pause: String(nextQuery.refreshPaused), - value: String(nextQuery.refreshInterval), + pause: currentQuery.refreshPaused + ? String(currentQuery.refreshPaused) + : '', + value: currentQuery.refreshInterval + ? String(currentQuery.refreshInterval) + : '', }, }; } diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts index 50325e0b9d604..186fc082ce5fe 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts @@ -17,6 +17,18 @@ describe('Transaction action menu', () => { const date = '2020-02-06T11:00:00.000Z'; const timestamp = { us: new Date(date).getTime() }; + const urlParams = { + rangeFrom: 'now-24h', + rangeTo: 'now', + refreshPaused: true, + refreshInterval: 0, + }; + + const location = ({ + search: + '?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0', + } as unknown) as Location; + it('shows required sections only', () => { const transaction = ({ timestamp, @@ -28,8 +40,8 @@ describe('Transaction action menu', () => { getSections({ transaction, basePath, - location: ({} as unknown) as Location, - urlParams: {}, + location, + urlParams, }) ).toEqual([ [ @@ -77,8 +89,8 @@ describe('Transaction action menu', () => { getSections({ transaction, basePath, - location: ({} as unknown) as Location, - urlParams: {}, + location, + urlParams, }) ).toEqual([ [ @@ -148,8 +160,8 @@ describe('Transaction action menu', () => { getSections({ transaction, basePath, - location: ({} as unknown) as Location, - urlParams: {}, + location, + urlParams, }) ).toEqual([ [ diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx index 922796afd39bf..7a5d0dd5ce877 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx @@ -44,7 +44,9 @@ describe('ErrorMarker', () => { return component; } function getKueryDecoded(url: string) { - return decodeURIComponent(url.substring(url.indexOf('kuery='), url.length)); + return decodeURIComponent( + url.substring(url.indexOf('kuery='), url.indexOf('&')) + ); } it('renders link with trace and transaction', () => { const component = openPopover(mark); diff --git a/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx b/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx index 329368e0c80f1..8c38cdcda958d 100644 --- a/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx +++ b/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx @@ -7,6 +7,30 @@ import React from 'react'; import { ApmPluginContext, ApmPluginContextValue } from '.'; import { createCallApmApi } from '../../services/rest/createCallApmApi'; import { ConfigSchema } from '../..'; +import { UI_SETTINGS } from '../../../../../../src/plugins/data/common'; + +const uiSettings: Record = { + [UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: [ + { + from: 'now/d', + to: 'now/d', + display: 'Today', + }, + { + from: 'now/w', + to: 'now/w', + display: 'This week', + }, + ], + [UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS]: { + from: 'now-15m', + to: 'now', + }, + [UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS]: { + pause: false, + value: 100000, + }, +}; const mockCore = { chrome: { @@ -27,6 +51,9 @@ const mockCore = { addDanger: () => {}, }, }, + uiSettings: { + get: (key: string) => uiSettings[key], + }, }; const mockConfig: ConfigSchema = { diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx b/x-pack/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx index b88e0b8e23ea5..fbb79eae6a136 100644 --- a/x-pack/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx @@ -53,15 +53,9 @@ describe('UrlParamsContext', () => { const params = getDataFromOutput(wrapper); expect(params).toEqual({ - start: '2000-06-14T12:00:00.000Z', serviceName: 'opbeans-node', - end: '2000-06-15T12:00:00.000Z', page: 0, processorEvent: 'transaction', - rangeFrom: 'now-24h', - rangeTo: 'now', - refreshInterval: 0, - refreshPaused: true, }); }); diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/constants.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/constants.ts index d654e60077be9..6297a560440d2 100644 --- a/x-pack/plugins/apm/public/context/UrlParamsContext/constants.ts +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/constants.ts @@ -6,9 +6,3 @@ export const TIME_RANGE_REFRESH = 'TIME_RANGE_REFRESH'; export const LOCATION_UPDATE = 'LOCATION_UPDATE'; -export const TIMEPICKER_DEFAULTS = { - rangeFrom: 'now-24h', - rangeTo: 'now', - refreshPaused: 'true', - refreshInterval: '0', -}; diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts index bae7b9a796e19..2201e162904a2 100644 --- a/x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts @@ -16,7 +16,6 @@ import { toString, } from './helpers'; import { toQuery } from '../../components/shared/Links/url_helpers'; -import { TIMEPICKER_DEFAULTS } from './constants'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { localUIFilterNames } from '../../../server/lib/ui_filters/local_ui_filters/config'; import { pickKeys } from '../../../common/utils/pick_keys'; @@ -51,10 +50,10 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { sortDirection, sortField, kuery, - refreshPaused = TIMEPICKER_DEFAULTS.refreshPaused, - refreshInterval = TIMEPICKER_DEFAULTS.refreshInterval, - rangeFrom = TIMEPICKER_DEFAULTS.rangeFrom, - rangeTo = TIMEPICKER_DEFAULTS.rangeTo, + refreshPaused, + refreshInterval, + rangeFrom, + rangeTo, environment, searchTerm, } = query; @@ -67,8 +66,8 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { end: getEnd(state, rangeTo), rangeFrom, rangeTo, - refreshPaused: toBoolean(refreshPaused), - refreshInterval: toNumber(refreshInterval), + refreshPaused: refreshPaused ? toBoolean(refreshPaused) : undefined, + refreshInterval: refreshInterval ? toNumber(refreshInterval) : undefined, // query params sortDirection, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a9a9e81f04b72..7191c88cab49d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4153,14 +4153,6 @@ "xpack.apm.customLink.buttom.create.title": "作成", "xpack.apm.customLink.buttom.manage": "カスタムリンクを管理", "xpack.apm.customLink.empty": "カスタムリンクが見つかりません。独自のカスタムリンク、たとえば特定のダッシュボードまたは外部リンクへのリンクをセットアップします。", - "xpack.apm.datePicker.last15MinutesLabel": "過去 15 分間", - "xpack.apm.datePicker.last1HourLabel": "過去 1 時間", - "xpack.apm.datePicker.last1YearLabel": "過去 1 年間", - "xpack.apm.datePicker.last24HoursLabel": "過去 24 時間", - "xpack.apm.datePicker.last30DaysLabel": "過去 30 日間", - "xpack.apm.datePicker.last30MinutesLabel": "過去 30 分間", - "xpack.apm.datePicker.last7DaysLabel": "過去 7 日間", - "xpack.apm.datePicker.last90DaysLabel": "過去 90 日間", "xpack.apm.emptyMessage.noDataFoundDescription": "別の時間範囲を試すか検索フィルターをリセットしてください。", "xpack.apm.emptyMessage.noDataFoundLabel": "データが見つかりません。", "xpack.apm.error.prompt.body": "詳細はブラウザの開発者コンソールをご確認ください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 95eb93d9e37bb..b4502781ddbb1 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4157,14 +4157,6 @@ "xpack.apm.customLink.buttom.create.title": "创建", "xpack.apm.customLink.buttom.manage": "管理定制链接", "xpack.apm.customLink.empty": "未找到定制链接。设置自己的定制链接,如特定仪表板的链接或外部链接。", - "xpack.apm.datePicker.last15MinutesLabel": "过去 15 分钟", - "xpack.apm.datePicker.last1HourLabel": "过去 1 小时", - "xpack.apm.datePicker.last1YearLabel": "过去 1 年", - "xpack.apm.datePicker.last24HoursLabel": "过去 24 小时", - "xpack.apm.datePicker.last30DaysLabel": "过去 30 天", - "xpack.apm.datePicker.last30MinutesLabel": "过去 30 分钟", - "xpack.apm.datePicker.last7DaysLabel": "过去 7 天", - "xpack.apm.datePicker.last90DaysLabel": "过去 90 天", "xpack.apm.emptyMessage.noDataFoundDescription": "尝试其他时间范围或重置搜索筛选。", "xpack.apm.emptyMessage.noDataFoundLabel": "未找到任何数据", "xpack.apm.error.prompt.body": "有关详情,请查看您的浏览器开发者控制台。", From 9c91fd9cb7aab4f46f0c6bee5ca5df049697c20c Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Tue, 14 Jul 2020 17:18:32 +0100 Subject: [PATCH 11/82] [SIEM] Add scripts for on boarding prepackage timeline (#67496) * add prepackaged timelines * generate ndjson * expose end api points * fix types * fix types * fix unit test * install prepackage timelines * plumbing for prepackaged timelines * read ndjson by line * fix unit test * update templates * fix types * fix types * fix integration test * update script * name it back * add timeline status into rule status api * fix update messages * fix unit tests * fix integration test * rename types * update prepackaged timelines * update prepackaged timelines script * update scripts * fix update for elastic template * move timeline utils * export timelines scripts * update module path * fix intefration test * add delete all timelines script * readme * add get_timeline_by_templatetimeline_id * add getTimelineByIdRoute * rename file * add unit test * fix types * fix types * update readme * fix error id * fix unit test * update path * update i18n * update readme * load prepacked timelines by default * add unit tests * Update x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/README.md Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> * review * update prepacked timelines Co-authored-by: Elastic Machine Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> --- .../security_solution/common/constants.ts | 1 + .../schemas/common/schemas.ts | 5 + .../response/prepackaged_rules_schema.test.ts | 56 +- .../response/prepackaged_rules_schema.ts | 28 +- .../prepackaged_rules_status_schema.test.ts | 53 +- .../prepackaged_rules_status_schema.ts | 29 +- .../common/types/timeline/index.ts | 32 +- .../rules/pre_packaged_rules/translations.ts | 46 +- .../update_callout.test.tsx | 1 + .../pre_packaged_rules/update_callout.tsx | 33 +- .../detection_engine/rules/__mocks__/api.ts | 3 + .../detection_engine/rules/translations.ts | 18 +- .../detection_engine/rules/types.ts | 3 + .../rules/use_pre_packaged_rules.test.tsx | 164 +-- .../rules/use_pre_packaged_rules.tsx | 57 +- .../detection_engine/rules/use_rule.tsx | 2 +- .../rules/use_rule_status.tsx | 4 +- .../detection_engine/rules/use_rules.tsx | 2 +- .../pages/detection_engine/rules/helpers.tsx | 46 + .../pages/detection_engine/rules/index.tsx | 45 +- .../detection_engine/rules/translations.ts | 25 +- .../public/graphql/introspection.json | 12 +- .../security_solution/public/graphql/types.ts | 4 +- .../components/open_timeline/index.tsx | 15 +- .../open_timeline/use_timeline_status.tsx | 9 + .../public/timelines/containers/api.ts | 18 + .../server/graphql/timeline/schema.gql.ts | 4 +- .../security_solution/server/graphql/types.ts | 4 +- .../routes/__mocks__/index.ts | 6 + .../rules/add_prepackaged_rules_route.test.ts | 132 ++- .../rules/add_prepackaged_rules_route.ts | 53 +- ...get_prepackaged_rules_status_route.test.ts | 25 +- .../get_prepackaged_rules_status_route.ts | 34 +- .../rules/prepackaged_timelines/README.md | 182 ++++ .../rules/prepackaged_timelines/endpoint.json | 1 + .../rules/prepackaged_timelines/index.ndjson | 12 + .../rules/prepackaged_timelines/network.json | 1 + .../rules/prepackaged_timelines/process.json | 1 + .../scripts/export_timelines_to_file.sh | 22 + ...dex.sh => regen_prepackage_rules_index.sh} | 2 +- .../timelines/add_prepackaged_timelines.sh | 21 + .../scripts/timelines/delete_all_timelines.sh | 28 + .../delete_timeline_by_timeline_id.sh | 21 + .../timelines/find_timeline_by_filter.sh | 34 + .../scripts/timelines/get_all_timelines.sh | 37 + .../scripts/timelines/get_timeline_by_id.sh | 19 + .../get_timeline_by_template_timeline_id.sh | 19 + .../regen_prepackage_timelines_index.sh | 25 + .../routes/__mocks__/import_timelines.ts | 948 +++++++++++++++++- .../__mocks__/prepackaged_timelines.ndjson | 1 + .../routes/__mocks__/request_responses.ts | 15 + .../routes/export_timelines_route.test.ts | 24 +- .../timeline/routes/export_timelines_route.ts | 15 +- .../routes/get_timeline_by_id_route.test.ts | 64 ++ .../routes/get_timeline_by_id_route.ts | 57 ++ .../routes/import_timelines_route.test.ts | 20 +- .../timeline/routes/import_timelines_route.ts | 229 +---- .../install_prepacked_timelines_route.test.ts | 110 ++ .../install_prepacked_timelines_route.ts | 90 ++ .../schemas/check_timelines_status_schema.ts | 18 + .../routes/schemas/export_timelines_schema.ts | 9 +- .../schemas/get_timeline_by_id_schema.ts | 13 + .../routes/schemas/import_timelines_schema.ts | 24 +- .../routes/utils/check_timelines_status.ts | 75 ++ .../lib/timeline/routes/utils/common.ts | 102 ++ .../timeline/routes/utils/create_timelines.ts | 21 +- .../timeline/routes/utils/export_timelines.ts | 144 +-- .../timeline/routes/utils/get_timelines.ts | 34 + .../routes/utils/get_timelines_from_stream.ts | 47 + .../routes/utils/get_timelines_to_install.ts | 20 + .../routes/utils/get_timelines_to_update.ts | 22 + .../timeline/routes/utils/import_timelines.ts | 270 ++++- .../utils/install_prepacked_timelines.test.ts | 257 +++++ .../utils/install_prepacked_timelines.ts | 51 + .../server/lib/timeline/saved_object.ts | 103 +- .../security_solution/server/routes/index.ts | 11 +- .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - .../basic/tests/add_prepackaged_rules.ts | 15 +- .../tests/get_prepackaged_rules_status.ts | 31 +- .../tests/add_prepackaged_rules.ts | 15 +- .../tests/get_prepackaged_rules_status.ts | 31 +- .../detection_engine_api_integration/utils.ts | 14 + 83 files changed, 3634 insertions(+), 666 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/README.md create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/endpoint.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/index.ndjson create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/network.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/process.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/export_timelines_to_file.sh rename x-pack/plugins/security_solution/server/lib/detection_engine/scripts/{regen_prepackge_rules_index.sh => regen_prepackage_rules_index.sh} (96%) create mode 100755 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/add_prepackaged_timelines.sh create mode 100755 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/delete_all_timelines.sh create mode 100755 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/delete_timeline_by_timeline_id.sh create mode 100755 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/find_timeline_by_filter.sh create mode 100755 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_all_timelines.sh create mode 100755 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_id.sh create mode 100755 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_template_timeline_id.sh create mode 100755 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/regen_prepackage_timelines_index.sh create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/prepackaged_timelines.ndjson create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.ts create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/routes/install_prepacked_timelines_route.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/routes/install_prepacked_timelines_route.ts create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/check_timelines_status_schema.ts create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/routes/utils/check_timelines_status.ts create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines.ts create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines_from_stream.ts create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines_to_install.ts create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines_to_update.ts create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/routes/utils/install_prepacked_timelines.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/routes/utils/install_prepacked_timelines.ts diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 516ee19dd3b03..e5dd109007eab 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -117,6 +117,7 @@ export const TIMELINE_URL = '/api/timeline'; export const TIMELINE_DRAFT_URL = `${TIMELINE_URL}/_draft`; export const TIMELINE_EXPORT_URL = `${TIMELINE_URL}/_export`; export const TIMELINE_IMPORT_URL = `${TIMELINE_URL}/_import`; +export const TIMELINE_PREPACKAGED_URL = `${TIMELINE_URL}/_prepackaged`; /** * Default signals index key for kibana.dev.yml diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index 6e43bd645fd7b..74c127365ddee 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -407,6 +407,11 @@ export const rules_custom_installed = PositiveInteger; export const rules_not_installed = PositiveInteger; export const rules_not_updated = PositiveInteger; +export const timelines_installed = PositiveInteger; +export const timelines_updated = PositiveInteger; +export const timelines_not_installed = PositiveInteger; +export const timelines_not_updated = PositiveInteger; + export const note = t.string; export type Note = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_schema.test.ts index fc3f89996daf1..61d3ede852ee1 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_schema.test.ts @@ -6,14 +6,22 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { PrePackagedRulesSchema, prePackagedRulesSchema } from './prepackaged_rules_schema'; +import { + PrePackagedRulesAndTimelinesSchema, + prePackagedRulesAndTimelinesSchema, +} from './prepackaged_rules_schema'; import { exactCheck } from '../../../exact_check'; import { foldLeftRight, getPaths } from '../../../test_utils'; describe('prepackaged_rules_schema', () => { test('it should validate an empty prepackaged response with defaults', () => { - const payload: PrePackagedRulesSchema = { rules_installed: 0, rules_updated: 0 }; - const decoded = prePackagedRulesSchema.decode(payload); + const payload: PrePackagedRulesAndTimelinesSchema = { + rules_installed: 0, + rules_updated: 0, + timelines_installed: 0, + timelines_updated: 0, + }; + const decoded = prePackagedRulesAndTimelinesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); @@ -22,12 +30,14 @@ describe('prepackaged_rules_schema', () => { }); test('it should not validate an extra invalid field added', () => { - const payload: PrePackagedRulesSchema & { invalid_field: string } = { + const payload: PrePackagedRulesAndTimelinesSchema & { invalid_field: string } = { rules_installed: 0, rules_updated: 0, invalid_field: 'invalid', + timelines_installed: 0, + timelines_updated: 0, }; - const decoded = prePackagedRulesSchema.decode(payload); + const decoded = prePackagedRulesAndTimelinesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); @@ -36,8 +46,13 @@ describe('prepackaged_rules_schema', () => { }); test('it should NOT validate an empty prepackaged response with a negative "rules_installed" number', () => { - const payload: PrePackagedRulesSchema = { rules_installed: -1, rules_updated: 0 }; - const decoded = prePackagedRulesSchema.decode(payload); + const payload: PrePackagedRulesAndTimelinesSchema = { + rules_installed: -1, + rules_updated: 0, + timelines_installed: 0, + timelines_updated: 0, + }; + const decoded = prePackagedRulesAndTimelinesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); @@ -48,8 +63,13 @@ describe('prepackaged_rules_schema', () => { }); test('it should NOT validate an empty prepackaged response with a negative "rules_updated"', () => { - const payload: PrePackagedRulesSchema = { rules_installed: 0, rules_updated: -1 }; - const decoded = prePackagedRulesSchema.decode(payload); + const payload: PrePackagedRulesAndTimelinesSchema = { + rules_installed: 0, + rules_updated: -1, + timelines_installed: 0, + timelines_updated: 0, + }; + const decoded = prePackagedRulesAndTimelinesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); @@ -60,9 +80,14 @@ describe('prepackaged_rules_schema', () => { }); test('it should NOT validate an empty prepackaged response if "rules_installed" is not there', () => { - const payload: PrePackagedRulesSchema = { rules_installed: 0, rules_updated: 0 }; + const payload: PrePackagedRulesAndTimelinesSchema = { + rules_installed: 0, + rules_updated: 0, + timelines_installed: 0, + timelines_updated: 0, + }; delete payload.rules_installed; - const decoded = prePackagedRulesSchema.decode(payload); + const decoded = prePackagedRulesAndTimelinesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); @@ -73,9 +98,14 @@ describe('prepackaged_rules_schema', () => { }); test('it should NOT validate an empty prepackaged response if "rules_updated" is not there', () => { - const payload: PrePackagedRulesSchema = { rules_installed: 0, rules_updated: 0 }; + const payload: PrePackagedRulesAndTimelinesSchema = { + rules_installed: 0, + rules_updated: 0, + timelines_installed: 0, + timelines_updated: 0, + }; delete payload.rules_updated; - const decoded = prePackagedRulesSchema.decode(payload); + const decoded = prePackagedRulesAndTimelinesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_schema.ts index 3b0107c91fee0..73d144500e003 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_schema.ts @@ -7,14 +7,28 @@ import * as t from 'io-ts'; /* eslint-disable @typescript-eslint/camelcase */ -import { rules_installed, rules_updated } from '../common/schemas'; +import { + rules_installed, + rules_updated, + timelines_installed, + timelines_updated, +} from '../common/schemas'; /* eslint-enable @typescript-eslint/camelcase */ -export const prePackagedRulesSchema = t.exact( - t.type({ - rules_installed, - rules_updated, - }) +const prePackagedRulesSchema = t.type({ + rules_installed, + rules_updated, +}); + +const prePackagedTimelinesSchema = t.type({ + timelines_installed, + timelines_updated, +}); + +export const prePackagedRulesAndTimelinesSchema = t.exact( + t.intersection([prePackagedRulesSchema, prePackagedTimelinesSchema]) ); -export type PrePackagedRulesSchema = t.TypeOf; +export type PrePackagedRulesAndTimelinesSchema = t.TypeOf< + typeof prePackagedRulesAndTimelinesSchema +>; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_status_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_status_schema.test.ts index eeae72209829e..09cb7148fe90a 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_status_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_status_schema.test.ts @@ -7,21 +7,24 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { - PrePackagedRulesStatusSchema, - prePackagedRulesStatusSchema, + PrePackagedRulesAndTimelinesStatusSchema, + prePackagedRulesAndTimelinesStatusSchema, } from './prepackaged_rules_status_schema'; import { exactCheck } from '../../../exact_check'; import { foldLeftRight, getPaths } from '../../../test_utils'; describe('prepackaged_rules_schema', () => { test('it should validate an empty prepackaged response with defaults', () => { - const payload: PrePackagedRulesStatusSchema = { + const payload: PrePackagedRulesAndTimelinesStatusSchema = { rules_installed: 0, rules_not_installed: 0, rules_not_updated: 0, rules_custom_installed: 0, + timelines_installed: 0, + timelines_not_installed: 0, + timelines_not_updated: 0, }; - const decoded = prePackagedRulesStatusSchema.decode(payload); + const decoded = prePackagedRulesAndTimelinesStatusSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); @@ -30,14 +33,17 @@ describe('prepackaged_rules_schema', () => { }); test('it should not validate an extra invalid field added', () => { - const payload: PrePackagedRulesStatusSchema & { invalid_field: string } = { + const payload: PrePackagedRulesAndTimelinesStatusSchema & { invalid_field: string } = { rules_installed: 0, rules_not_installed: 0, rules_not_updated: 0, rules_custom_installed: 0, invalid_field: 'invalid', + timelines_installed: 0, + timelines_not_installed: 0, + timelines_not_updated: 0, }; - const decoded = prePackagedRulesStatusSchema.decode(payload); + const decoded = prePackagedRulesAndTimelinesStatusSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); @@ -46,13 +52,16 @@ describe('prepackaged_rules_schema', () => { }); test('it should NOT validate an empty prepackaged response with a negative "rules_installed" number', () => { - const payload: PrePackagedRulesStatusSchema = { + const payload: PrePackagedRulesAndTimelinesStatusSchema = { rules_installed: -1, rules_not_installed: 0, rules_not_updated: 0, rules_custom_installed: 0, + timelines_installed: 0, + timelines_not_installed: 0, + timelines_not_updated: 0, }; - const decoded = prePackagedRulesStatusSchema.decode(payload); + const decoded = prePackagedRulesAndTimelinesStatusSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); @@ -63,13 +72,16 @@ describe('prepackaged_rules_schema', () => { }); test('it should NOT validate an empty prepackaged response with a negative "rules_not_installed"', () => { - const payload: PrePackagedRulesStatusSchema = { + const payload: PrePackagedRulesAndTimelinesStatusSchema = { rules_installed: 0, rules_not_installed: -1, rules_not_updated: 0, rules_custom_installed: 0, + timelines_installed: 0, + timelines_not_installed: 0, + timelines_not_updated: 0, }; - const decoded = prePackagedRulesStatusSchema.decode(payload); + const decoded = prePackagedRulesAndTimelinesStatusSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); @@ -80,13 +92,16 @@ describe('prepackaged_rules_schema', () => { }); test('it should NOT validate an empty prepackaged response with a negative "rules_not_updated"', () => { - const payload: PrePackagedRulesStatusSchema = { + const payload: PrePackagedRulesAndTimelinesStatusSchema = { rules_installed: 0, rules_not_installed: 0, rules_not_updated: -1, rules_custom_installed: 0, + timelines_installed: 0, + timelines_not_installed: 0, + timelines_not_updated: 0, }; - const decoded = prePackagedRulesStatusSchema.decode(payload); + const decoded = prePackagedRulesAndTimelinesStatusSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); @@ -97,13 +112,16 @@ describe('prepackaged_rules_schema', () => { }); test('it should NOT validate an empty prepackaged response with a negative "rules_custom_installed"', () => { - const payload: PrePackagedRulesStatusSchema = { + const payload: PrePackagedRulesAndTimelinesStatusSchema = { rules_installed: 0, rules_not_installed: 0, rules_not_updated: 0, rules_custom_installed: -1, + timelines_installed: 0, + timelines_not_installed: 0, + timelines_not_updated: 0, }; - const decoded = prePackagedRulesStatusSchema.decode(payload); + const decoded = prePackagedRulesAndTimelinesStatusSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); @@ -114,14 +132,17 @@ describe('prepackaged_rules_schema', () => { }); test('it should NOT validate an empty prepackaged response if "rules_installed" is not there', () => { - const payload: PrePackagedRulesStatusSchema = { + const payload: PrePackagedRulesAndTimelinesStatusSchema = { rules_installed: 0, rules_not_installed: 0, rules_not_updated: 0, rules_custom_installed: 0, + timelines_installed: 0, + timelines_not_installed: 0, + timelines_not_updated: 0, }; delete payload.rules_installed; - const decoded = prePackagedRulesStatusSchema.decode(payload); + const decoded = prePackagedRulesAndTimelinesStatusSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_status_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_status_schema.ts index ee8e7b48a58bc..aabdbdd7300f4 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_status_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_status_schema.ts @@ -12,16 +12,29 @@ import { rules_custom_installed, rules_not_installed, rules_not_updated, + timelines_installed, + timelines_not_installed, + timelines_not_updated, } from '../common/schemas'; /* eslint-enable @typescript-eslint/camelcase */ -export const prePackagedRulesStatusSchema = t.exact( - t.type({ - rules_custom_installed, - rules_installed, - rules_not_installed, - rules_not_updated, - }) +export const prePackagedTimelinesStatusSchema = t.type({ + timelines_installed, + timelines_not_installed, + timelines_not_updated, +}); + +const prePackagedRulesStatusSchema = t.type({ + rules_custom_installed, + rules_installed, + rules_not_installed, + rules_not_updated, +}); + +export const prePackagedRulesAndTimelinesStatusSchema = t.exact( + t.intersection([prePackagedRulesStatusSchema, prePackagedTimelinesStatusSchema]) ); -export type PrePackagedRulesStatusSchema = t.TypeOf; +export type PrePackagedRulesAndTimelinesStatusSchema = t.TypeOf< + typeof prePackagedRulesAndTimelinesStatusSchema +>; diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 9e7a6f46bbcec..021e5a7f00b17 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -7,11 +7,16 @@ /* eslint-disable @typescript-eslint/camelcase, @typescript-eslint/no-empty-interface */ import * as runtimeTypes from 'io-ts'; -import { SavedObjectsClient } from 'kibana/server'; import { stringEnum, unionWithNullType } from '../../utility_types'; import { NoteSavedObject, NoteSavedObjectToReturnRuntimeType } from './note'; import { PinnedEventToReturnSavedObjectRuntimeType, PinnedEventSavedObject } from './pinned_event'; +import { + success, + success_count as successCount, +} from '../../detection_engine/schemas/common/schemas'; +import { PositiveInteger } from '../../detection_engine/schemas/types'; +import { errorSchema } from '../../detection_engine/schemas/response/error_schema'; /* * ColumnHeader Types @@ -353,19 +358,6 @@ export interface AllTimelineSavedObject * Import/export timelines */ -export type ExportTimelineSavedObjectsClient = Pick< - SavedObjectsClient, - | 'get' - | 'errors' - | 'create' - | 'bulkCreate' - | 'delete' - | 'find' - | 'bulkGet' - | 'update' - | 'bulkUpdate' ->; - export type ExportedGlobalNotes = Array>; export type ExportedEventNotes = NoteSavedObject[]; @@ -393,3 +385,15 @@ export type NotesAndPinnedEventsByTimelineId = Record< string, { notes: NoteSavedObject[]; pinnedEvents: PinnedEventSavedObject[] } >; + +export const importTimelineResultSchema = runtimeTypes.exact( + runtimeTypes.type({ + success, + success_count: successCount, + timelines_installed: PositiveInteger, + timelines_updated: PositiveInteger, + errors: runtimeTypes.array(errorSchema), + }) +); + +export type ImportTimelineResultSchema = runtimeTypes.TypeOf; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts index 4d2ba8b861cce..37c1715c05d71 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts @@ -38,7 +38,7 @@ export const CREATE_RULE_ACTION = i18n.translate( export const UPDATE_PREPACKAGED_RULES_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesTitle', { - defaultMessage: 'Update available for Elastic prebuilt rules', + defaultMessage: 'Update available for Elastic prebuilt rules or timeline templates', } ); @@ -46,16 +46,56 @@ export const UPDATE_PREPACKAGED_RULES_MSG = (updateRules: number) => i18n.translate('xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesMsg', { values: { updateRules }, defaultMessage: - 'You can update {updateRules} Elastic prebuilt {updateRules, plural, =1 {rule} other {rules}}. Note that this will reload deleted Elastic prebuilt rules.', + 'You can update {updateRules} Elastic prebuilt {updateRules, plural, =1 {rule} other {rules}}', }); +export const UPDATE_PREPACKAGED_TIMELINES_MSG = (updateTimelines: number) => + i18n.translate('xpack.securitySolution.detectionEngine.rules.updatePrePackagedTimelinesMsg', { + values: { updateTimelines }, + defaultMessage: + 'You can update {updateTimelines} Elastic prebuilt {updateTimelines, plural, =1 {timeline} other {timelines}}', + }); + +export const UPDATE_PREPACKAGED_RULES_AND_TIMELINES_MSG = ( + updateRules: number, + updateTimelines: number +) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesAndTimelinesMsg', + { + values: { updateRules, updateTimelines }, + defaultMessage: + 'You can update {updateRules} Elastic prebuilt {updateRules, plural, =1 {rule} other {rules}} and {updateTimelines} Elastic prebuilt {updateTimelines, plural, =1 {timeline} other {timelines}}. Note that this will reload deleted Elastic prebuilt rules.', + } + ); + export const UPDATE_PREPACKAGED_RULES = (updateRules: number) => i18n.translate('xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesButton', { values: { updateRules }, defaultMessage: - 'Update {updateRules} Elastic prebuilt {updateRules, plural, =1 {rule} other {rules}} ', + 'Update {updateRules} Elastic prebuilt {updateRules, plural, =1 {rule} other {rules}}', }); +export const UPDATE_PREPACKAGED_TIMELINES = (updateTimelines: number) => + i18n.translate('xpack.securitySolution.detectionEngine.rules.updatePrePackagedTimelinesButton', { + values: { updateTimelines }, + defaultMessage: + 'Update {updateTimelines} Elastic prebuilt {updateTimelines, plural, =1 {timeline} other {timelines}}', + }); + +export const UPDATE_PREPACKAGED_RULES_AND_TIMELINES = ( + updateRules: number, + updateTimelines: number +) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.updatePrePackagedRulesAndTimelinesButton', + { + values: { updateRules, updateTimelines }, + defaultMessage: + 'Update {updateRules} Elastic prebuilt {updateRules, plural, =1 {rule} other {rules}} and {updateTimelines} Elastic prebuilt {updateTimelines, plural, =1 {timeline} other {timelines}}', + } + ); + export const RELEASE_NOTES_HELP = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.releaseNotesHelp', { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.test.tsx index b5dca70ad9575..5033fcd11dc7c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.test.tsx @@ -27,6 +27,7 @@ describe('UpdatePrePackagedRulesCallOut', () => { ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.tsx index 0faf4074ed890..3be2b853925f6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo } from 'react'; +import React, { memo, useMemo } from 'react'; import { EuiCallOut, EuiButton, EuiLink } from '@elastic/eui'; @@ -14,19 +14,46 @@ import * as i18n from './translations'; interface UpdatePrePackagedRulesCallOutProps { loading: boolean; numberOfUpdatedRules: number; + numberOfUpdatedTimelines: number; updateRules: () => void; } const UpdatePrePackagedRulesCallOutComponent: React.FC = ({ loading, numberOfUpdatedRules, + numberOfUpdatedTimelines, updateRules, }) => { const { services } = useKibana(); + + const prepackagedRulesOrTimelines = useMemo(() => { + if (numberOfUpdatedRules > 0 && numberOfUpdatedTimelines === 0) { + return { + callOutMessage: i18n.UPDATE_PREPACKAGED_RULES_MSG(numberOfUpdatedRules), + buttonTitle: i18n.UPDATE_PREPACKAGED_RULES(numberOfUpdatedRules), + }; + } else if (numberOfUpdatedRules === 0 && numberOfUpdatedTimelines > 0) { + return { + callOutMessage: i18n.UPDATE_PREPACKAGED_TIMELINES_MSG(numberOfUpdatedTimelines), + buttonTitle: i18n.UPDATE_PREPACKAGED_TIMELINES(numberOfUpdatedTimelines), + }; + } else if (numberOfUpdatedRules > 0 && numberOfUpdatedTimelines > 0) + return { + callOutMessage: i18n.UPDATE_PREPACKAGED_RULES_AND_TIMELINES_MSG( + numberOfUpdatedRules, + numberOfUpdatedTimelines + ), + buttonTitle: i18n.UPDATE_PREPACKAGED_RULES_AND_TIMELINES( + numberOfUpdatedRules, + numberOfUpdatedTimelines + ), + }; + }, [numberOfUpdatedRules, numberOfUpdatedTimelines]); + return (

- {i18n.UPDATE_PREPACKAGED_RULES_MSG(numberOfUpdatedRules)} + {prepackagedRulesOrTimelines?.callOutMessage}

- {i18n.UPDATE_PREPACKAGED_RULES(numberOfUpdatedRules)} + {prepackagedRulesOrTimelines?.buttonTitle}
); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts index 3275391f3f074..f12a5d523bade 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts @@ -34,6 +34,9 @@ export const getPrePackagedRulesStatus = async ({ rules_installed: 12, rules_not_installed: 0, rules_not_updated: 0, + timelines_installed: 0, + timelines_not_installed: 0, + timelines_not_updated: 0, }); export const createPrepackagedRules = async ({ signal }: BasicFetchProps): Promise => diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts index 2b4b32bce9c7b..f878b40b99dc3 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts @@ -6,10 +6,10 @@ import { i18n } from '@kbn/i18n'; -export const RULE_FETCH_FAILURE = i18n.translate( - 'xpack.securitySolution.containers.detectionEngine.rules', +export const RULE_AND_TIMELINE_FETCH_FAILURE = i18n.translate( + 'xpack.securitySolution.containers.detectionEngine.rulesAndTimelines', { - defaultMessage: 'Failed to fetch Rules', + defaultMessage: 'Failed to fetch Rules and Timelines', } ); @@ -20,17 +20,17 @@ export const RULE_ADD_FAILURE = i18n.translate( } ); -export const RULE_PREPACKAGED_FAILURE = i18n.translate( - 'xpack.securitySolution.containers.detectionEngine.createPrePackagedRuleFailDescription', +export const RULE_AND_TIMELINE_PREPACKAGED_FAILURE = i18n.translate( + 'xpack.securitySolution.containers.detectionEngine.createPrePackagedRuleAndTimelineFailDescription', { - defaultMessage: 'Failed to installed pre-packaged rules from elastic', + defaultMessage: 'Failed to installed pre-packaged rules and timelines from elastic', } ); -export const RULE_PREPACKAGED_SUCCESS = i18n.translate( - 'xpack.securitySolution.containers.detectionEngine.createPrePackagedRuleSuccesDescription', +export const RULE_AND_TIMELINE_PREPACKAGED_SUCCESS = i18n.translate( + 'xpack.securitySolution.containers.detectionEngine.createPrePackagedRuleAndTimelineSuccesDescription', { - defaultMessage: 'Installed pre-packaged rules from elastic', + defaultMessage: 'Installed pre-packaged rules and timelines from elastic', } ); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index c03d19eaf771e..5c876625cf9f9 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -273,4 +273,7 @@ export interface PrePackagedRulesStatusResponse { rules_installed: number; rules_not_installed: number; rules_not_updated: number; + timelines_installed: number; + timelines_not_installed: number; + timelines_not_updated: number; } diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx index 4d9e283bfb9cc..9a6ea4f60fdcc 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx @@ -5,7 +5,7 @@ */ import { renderHook, act } from '@testing-library/react-hooks'; -import { ReturnPrePackagedRules, usePrePackagedRules } from './use_pre_packaged_rules'; +import { ReturnPrePackagedRulesAndTimelines, usePrePackagedRules } from './use_pre_packaged_rules'; import * as api from './api'; jest.mock('./api'); @@ -18,14 +18,15 @@ describe('usePersistRule', () => { test('init', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - usePrePackagedRules({ - canUserCRUD: null, - hasIndexWrite: null, - isAuthenticated: null, - hasEncryptionKey: null, - isSignalIndexExists: null, - }) + const { result, waitForNextUpdate } = renderHook( + () => + usePrePackagedRules({ + canUserCRUD: null, + hasIndexWrite: null, + isAuthenticated: null, + hasEncryptionKey: null, + isSignalIndexExists: null, + }) ); await waitForNextUpdate(); @@ -39,20 +40,24 @@ describe('usePersistRule', () => { rulesInstalled: null, rulesNotInstalled: null, rulesNotUpdated: null, + timelinesInstalled: null, + timelinesNotInstalled: null, + timelinesNotUpdated: null, }); }); }); test('fetch getPrePackagedRulesStatus', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - usePrePackagedRules({ - canUserCRUD: null, - hasIndexWrite: null, - isAuthenticated: null, - hasEncryptionKey: null, - isSignalIndexExists: null, - }) + const { result, waitForNextUpdate } = renderHook( + () => + usePrePackagedRules({ + canUserCRUD: null, + hasIndexWrite: null, + isAuthenticated: null, + hasEncryptionKey: null, + isSignalIndexExists: null, + }) ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -66,6 +71,9 @@ describe('usePersistRule', () => { rulesInstalled: 12, rulesNotInstalled: 0, rulesNotUpdated: 0, + timelinesInstalled: 0, + timelinesNotInstalled: 0, + timelinesNotUpdated: 0, }); }); }); @@ -73,14 +81,15 @@ describe('usePersistRule', () => { test('happy path to createPrePackagedRules', async () => { const spyOnCreatePrepackagedRules = jest.spyOn(api, 'createPrepackagedRules'); await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - usePrePackagedRules({ - canUserCRUD: true, - hasIndexWrite: true, - isAuthenticated: true, - hasEncryptionKey: true, - isSignalIndexExists: true, - }) + const { result, waitForNextUpdate } = renderHook( + () => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -99,6 +108,9 @@ describe('usePersistRule', () => { rulesInstalled: 12, rulesNotInstalled: 0, rulesNotUpdated: 0, + timelinesInstalled: 0, + timelinesNotInstalled: 0, + timelinesNotUpdated: 0, }); }); }); @@ -109,14 +121,15 @@ describe('usePersistRule', () => { throw new Error('Something went wrong'); }); await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - usePrePackagedRules({ - canUserCRUD: true, - hasIndexWrite: true, - isAuthenticated: true, - hasEncryptionKey: true, - isSignalIndexExists: true, - }) + const { result, waitForNextUpdate } = renderHook( + () => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -131,14 +144,15 @@ describe('usePersistRule', () => { test('can NOT createPrePackagedRules because canUserCrud === false', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - usePrePackagedRules({ - canUserCRUD: false, - hasIndexWrite: true, - isAuthenticated: true, - hasEncryptionKey: true, - isSignalIndexExists: true, - }) + const { result, waitForNextUpdate } = renderHook( + () => + usePrePackagedRules({ + canUserCRUD: false, + hasIndexWrite: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -152,14 +166,15 @@ describe('usePersistRule', () => { test('can NOT createPrePackagedRules because hasIndexWrite === false', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - usePrePackagedRules({ - canUserCRUD: true, - hasIndexWrite: false, - isAuthenticated: true, - hasEncryptionKey: true, - isSignalIndexExists: true, - }) + const { result, waitForNextUpdate } = renderHook( + () => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: false, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -173,14 +188,15 @@ describe('usePersistRule', () => { test('can NOT createPrePackagedRules because isAuthenticated === false', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - usePrePackagedRules({ - canUserCRUD: true, - hasIndexWrite: true, - isAuthenticated: false, - hasEncryptionKey: true, - isSignalIndexExists: true, - }) + const { result, waitForNextUpdate } = renderHook( + () => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + isAuthenticated: false, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -194,14 +210,15 @@ describe('usePersistRule', () => { test('can NOT createPrePackagedRules because hasEncryptionKey === false', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - usePrePackagedRules({ - canUserCRUD: true, - hasIndexWrite: true, - isAuthenticated: true, - hasEncryptionKey: false, - isSignalIndexExists: true, - }) + const { result, waitForNextUpdate } = renderHook( + () => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + isAuthenticated: true, + hasEncryptionKey: false, + isSignalIndexExists: true, + }) ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -215,14 +232,15 @@ describe('usePersistRule', () => { test('can NOT createPrePackagedRules because isSignalIndexExists === false', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - usePrePackagedRules({ - canUserCRUD: true, - hasIndexWrite: true, - isAuthenticated: true, - hasEncryptionKey: true, - isSignalIndexExists: false, - }) + const { result, waitForNextUpdate } = renderHook( + () => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: false, + }) ); await waitForNextUpdate(); await waitForNextUpdate(); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx index 5f5ee53c29caf..08c85695e9313 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx @@ -16,7 +16,14 @@ import * as i18n from './translations'; type Func = () => void; export type CreatePreBuiltRules = () => Promise; -export interface ReturnPrePackagedRules { + +interface ReturnPrePackagedTimelines { + timelinesInstalled: number | null; + timelinesNotInstalled: number | null; + timelinesNotUpdated: number | null; +} + +interface ReturnPrePackagedRules { createPrePackagedRules: null | CreatePreBuiltRules; loading: boolean; loadingCreatePrePackagedRules: boolean; @@ -27,6 +34,9 @@ export interface ReturnPrePackagedRules { rulesNotUpdated: number | null; } +export type ReturnPrePackagedRulesAndTimelines = ReturnPrePackagedRules & + ReturnPrePackagedTimelines; + interface UsePrePackagedRuleProps { canUserCRUD: boolean | null; hasIndexWrite: boolean | null; @@ -50,16 +60,19 @@ export const usePrePackagedRules = ({ isAuthenticated, hasEncryptionKey, isSignalIndexExists, -}: UsePrePackagedRuleProps): ReturnPrePackagedRules => { - const [rulesStatus, setRuleStatus] = useState< +}: UsePrePackagedRuleProps): ReturnPrePackagedRulesAndTimelines => { + const [prepackagedDataStatus, setPrepackagedDataStatus] = useState< Pick< - ReturnPrePackagedRules, + ReturnPrePackagedRulesAndTimelines, | 'createPrePackagedRules' | 'refetchPrePackagedRulesStatus' | 'rulesCustomInstalled' | 'rulesInstalled' | 'rulesNotInstalled' | 'rulesNotUpdated' + | 'timelinesInstalled' + | 'timelinesNotInstalled' + | 'timelinesNotUpdated' > >({ createPrePackagedRules: null, @@ -68,7 +81,11 @@ export const usePrePackagedRules = ({ rulesInstalled: null, rulesNotInstalled: null, rulesNotUpdated: null, + timelinesInstalled: null, + timelinesNotInstalled: null, + timelinesNotUpdated: null, }); + const [loadingCreatePrePackagedRules, setLoadingCreatePrePackagedRules] = useState(false); const [loading, setLoading] = useState(true); const [, dispatchToaster] = useStateToaster(); @@ -85,26 +102,33 @@ export const usePrePackagedRules = ({ }); if (isSubscribed) { - setRuleStatus({ + setPrepackagedDataStatus({ createPrePackagedRules: createElasticRules, refetchPrePackagedRulesStatus: fetchPrePackagedRules, rulesCustomInstalled: prePackagedRuleStatusResponse.rules_custom_installed, rulesInstalled: prePackagedRuleStatusResponse.rules_installed, rulesNotInstalled: prePackagedRuleStatusResponse.rules_not_installed, rulesNotUpdated: prePackagedRuleStatusResponse.rules_not_updated, + timelinesInstalled: prePackagedRuleStatusResponse.timelines_installed, + timelinesNotInstalled: prePackagedRuleStatusResponse.timelines_not_installed, + timelinesNotUpdated: prePackagedRuleStatusResponse.timelines_not_updated, }); } } catch (error) { if (isSubscribed) { - setRuleStatus({ + setPrepackagedDataStatus({ createPrePackagedRules: null, refetchPrePackagedRulesStatus: null, rulesCustomInstalled: null, rulesInstalled: null, rulesNotInstalled: null, rulesNotUpdated: null, + timelinesInstalled: null, + timelinesNotInstalled: null, + timelinesNotUpdated: null, }); - errorToToaster({ title: i18n.RULE_FETCH_FAILURE, error, dispatchToaster }); + + errorToToaster({ title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE, error, dispatchToaster }); } } if (isSubscribed) { @@ -149,15 +173,22 @@ export const usePrePackagedRules = ({ iterationTryOfFetchingPrePackagedCount > 100) ) { setLoadingCreatePrePackagedRules(false); - setRuleStatus({ + setPrepackagedDataStatus({ createPrePackagedRules: createElasticRules, refetchPrePackagedRulesStatus: fetchPrePackagedRules, rulesCustomInstalled: prePackagedRuleStatusResponse.rules_custom_installed, rulesInstalled: prePackagedRuleStatusResponse.rules_installed, rulesNotInstalled: prePackagedRuleStatusResponse.rules_not_installed, rulesNotUpdated: prePackagedRuleStatusResponse.rules_not_updated, + timelinesInstalled: prePackagedRuleStatusResponse.timelines_installed, + timelinesNotInstalled: prePackagedRuleStatusResponse.timelines_not_installed, + timelinesNotUpdated: prePackagedRuleStatusResponse.timelines_not_updated, }); - displaySuccessToast(i18n.RULE_PREPACKAGED_SUCCESS, dispatchToaster); + + displaySuccessToast( + i18n.RULE_AND_TIMELINE_PREPACKAGED_SUCCESS, + dispatchToaster + ); stopTimeOut(); resolve(true); } else { @@ -172,7 +203,11 @@ export const usePrePackagedRules = ({ } catch (error) { if (isSubscribed) { setLoadingCreatePrePackagedRules(false); - errorToToaster({ title: i18n.RULE_PREPACKAGED_FAILURE, error, dispatchToaster }); + errorToToaster({ + title: i18n.RULE_AND_TIMELINE_PREPACKAGED_FAILURE, + error, + dispatchToaster, + }); resolve(false); } } @@ -191,6 +226,6 @@ export const usePrePackagedRules = ({ return { loading, loadingCreatePrePackagedRules, - ...rulesStatus, + ...prepackagedDataStatus, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.tsx index 3256273fb8425..706c2645a4ddd 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.tsx @@ -41,7 +41,7 @@ export const useRule = (id: string | undefined): ReturnRule => { } catch (error) { if (isSubscribed) { setRule(null); - errorToToaster({ title: i18n.RULE_FETCH_FAILURE, error, dispatchToaster }); + errorToToaster({ title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE, error, dispatchToaster }); } } if (isSubscribed) { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx index ec1da29de4ba8..0e96f58ee6874 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx @@ -49,7 +49,7 @@ export const useRuleStatus = (id: string | undefined | null): ReturnRuleStatus = } catch (error) { if (isSubscribed) { setRuleStatus(null); - errorToToaster({ title: i18n.RULE_FETCH_FAILURE, error, dispatchToaster }); + errorToToaster({ title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE, error, dispatchToaster }); } } if (isSubscribed) { @@ -106,7 +106,7 @@ export const useRulesStatuses = (rules: Rules): ReturnRulesStatuses => { } catch (error) { if (isSubscribed) { setRuleStatuses([]); - errorToToaster({ title: i18n.RULE_FETCH_FAILURE, error, dispatchToaster }); + errorToToaster({ title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE, error, dispatchToaster }); } } if (isSubscribed) { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rules.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rules.tsx index 1a1dbc6e2b368..3466472ad7276 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rules.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rules.tsx @@ -67,7 +67,7 @@ export const useRules = ({ } } catch (error) { if (isSubscribed) { - errorToToaster({ title: i18n.RULE_FETCH_FAILURE, error, dispatchToaster }); + errorToToaster({ title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE, error, dispatchToaster }); if (dispatchRulesInReducer != null) { dispatchRulesInReducer([], {}); } 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 6a98280076b30..ce37b02a0b5ae 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 @@ -182,6 +182,13 @@ export type PrePackagedRuleStatus = | 'someRuleUninstall' | 'unknown'; +export type PrePackagedTimelineStatus = + | 'timelinesNotInstalled' + | 'timelinesInstalled' + | 'someTimelineUninstall' + | 'timelineNeedUpdate' + | 'unknown'; + export const getPrePackagedRuleStatus = ( rulesInstalled: number | null, rulesNotInstalled: number | null, @@ -221,6 +228,45 @@ export const getPrePackagedRuleStatus = ( } return 'unknown'; }; +export const getPrePackagedTimelineStatus = ( + timelinesInstalled: number | null, + timelinesNotInstalled: number | null, + timelinesNotUpdated: number | null +): PrePackagedTimelineStatus => { + if ( + timelinesNotInstalled != null && + timelinesInstalled === 0 && + timelinesNotInstalled > 0 && + timelinesNotUpdated === 0 + ) { + return 'timelinesNotInstalled'; + } else if ( + timelinesInstalled != null && + timelinesInstalled > 0 && + timelinesNotInstalled === 0 && + timelinesNotUpdated === 0 + ) { + return 'timelinesInstalled'; + } else if ( + timelinesInstalled != null && + timelinesNotInstalled != null && + timelinesInstalled > 0 && + timelinesNotInstalled > 0 && + timelinesNotUpdated === 0 + ) { + return 'someTimelineUninstall'; + } else if ( + timelinesInstalled != null && + timelinesNotInstalled != null && + timelinesNotUpdated != null && + timelinesInstalled > 0 && + timelinesNotInstalled >= 0 && + timelinesNotUpdated > 0 + ) { + return 'timelineNeedUpdate'; + } + return 'unknown'; +}; export const setFieldValue = ( form: FormHook, schema: FormSchema, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx index 0fce9e5ea3a44..c1d8436a7230e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx @@ -24,7 +24,12 @@ import { ImportDataModal } from '../../../../common/components/import_data_modal import { ReadOnlyCallOut } from '../../../components/rules/read_only_callout'; import { ValueListsModal } from '../../../components/value_lists_management_modal'; import { UpdatePrePackagedRulesCallOut } from '../../../components/rules/pre_packaged_rules/update_callout'; -import { getPrePackagedRuleStatus, redirectToDetections, userHasNoPermissions } from './helpers'; +import { + getPrePackagedRuleStatus, + getPrePackagedTimelineStatus, + redirectToDetections, + userHasNoPermissions, +} from './helpers'; import * as i18n from './translations'; import { SecurityPageName } from '../../../../app/types'; import { LinkButton } from '../../../../common/components/links'; @@ -61,6 +66,9 @@ const RulesPageComponent: React.FC = () => { rulesInstalled, rulesNotInstalled, rulesNotUpdated, + timelinesInstalled, + timelinesNotInstalled, + timelinesNotUpdated, } = usePrePackagedRules({ canUserCRUD, hasIndexWrite, @@ -68,13 +76,19 @@ const RulesPageComponent: React.FC = () => { isAuthenticated, hasEncryptionKey, }); - const { formatUrl } = useFormatUrl(SecurityPageName.detections); const prePackagedRuleStatus = getPrePackagedRuleStatus( rulesInstalled, rulesNotInstalled, rulesNotUpdated ); + const prePackagedTimelineStatus = getPrePackagedTimelineStatus( + timelinesInstalled, + timelinesNotInstalled, + timelinesNotUpdated + ); + const { formatUrl } = useFormatUrl(SecurityPageName.detections); + const handleRefreshRules = useCallback(async () => { if (refreshRulesData.current != null) { refreshRulesData.current(true); @@ -98,6 +112,18 @@ const RulesPageComponent: React.FC = () => { refreshRulesData.current = refreshRule; }, []); + const getMissingRulesOrTimelinesButtonTitle = useCallback( + (missingRules: number, missingTimelines: number) => { + if (missingRules > 0 && missingTimelines === 0) + return i18n.RELOAD_MISSING_PREPACKAGED_RULES(missingRules); + else if (missingRules === 0 && missingTimelines > 0) + return i18n.RELOAD_MISSING_PREPACKAGED_TIMELINES(missingTimelines); + else if (missingRules > 0 && missingTimelines > 0) + return i18n.RELOAD_MISSING_PREPACKAGED_RULES_AND_TIMELINES(missingRules, missingTimelines); + }, + [] + ); + const goToNewRule = useCallback( (ev) => { ev.preventDefault(); @@ -147,7 +173,8 @@ const RulesPageComponent: React.FC = () => { title={i18n.PAGE_TITLE} > - {prePackagedRuleStatus === 'ruleNotInstalled' && ( + {(prePackagedRuleStatus === 'ruleNotInstalled' || + prePackagedTimelineStatus === 'timelinesNotInstalled') && ( { )} - {prePackagedRuleStatus === 'someRuleUninstall' && ( + {(prePackagedRuleStatus === 'someRuleUninstall' || + prePackagedTimelineStatus === 'someTimelineUninstall') && ( { isDisabled={userHasNoPermissions(canUserCRUD) || loading} onClick={handleCreatePrePackagedRules} > - {i18n.RELOAD_MISSING_PREPACKAGED_RULES(rulesNotInstalled ?? 0)} + {getMissingRulesOrTimelinesButtonTitle( + rulesNotInstalled ?? 0, + timelinesNotInstalled ?? 0 + )} )} @@ -206,10 +237,12 @@ const RulesPageComponent: React.FC = () => { - {prePackagedRuleStatus === 'ruleNeedUpdate' && ( + {(prePackagedRuleStatus === 'ruleNeedUpdate' || + prePackagedTimelineStatus === 'timelineNeedUpdate') && ( )} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index 5e6f8ac896e34..4f292b1bbbab8 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -468,7 +468,7 @@ export const DELETE = i18n.translate( export const LOAD_PREPACKAGED_RULES = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.loadPrePackagedRulesButton', { - defaultMessage: 'Load Elastic prebuilt rules', + defaultMessage: 'Load Elastic prebuilt rules and timeline templates', } ); @@ -482,6 +482,29 @@ export const RELOAD_MISSING_PREPACKAGED_RULES = (missingRules: number) => } ); +export const RELOAD_MISSING_PREPACKAGED_TIMELINES = (missingTimelines: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedTimelinesButton', + { + values: { missingTimelines }, + defaultMessage: + 'Install {missingTimelines} Elastic prebuilt {missingTimelines, plural, =1 {timeline} other {timelines}} ', + } + ); + +export const RELOAD_MISSING_PREPACKAGED_RULES_AND_TIMELINES = ( + missingRules: number, + missingTimelines: number +) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesAndTimelinesButton', + { + values: { missingRules, missingTimelines }, + defaultMessage: + 'Install {missingRules} Elastic prebuilt {missingRules, plural, =1 {rule} other {rules}} and {missingTimelines} Elastic prebuilt {missingTimelines, plural, =1 {timeline} other {timelines}} ', + } + ); + export const IMPORT_RULE_BTN_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.components.importRuleModal.importRuleTitle', { diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 20978fa3b063c..d5fbf4d865ac5 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -10593,21 +10593,13 @@ { "name": "pageIndex", "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, "defaultValue": null }, { "name": "pageSize", "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, "defaultValue": null } ], diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 27aa02038097e..429590ffc3e7d 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -96,9 +96,9 @@ export interface TlsSortField { } export interface PageInfoTimeline { - pageIndex: number; + pageIndex?: Maybe; - pageSize: number; + pageSize?: Maybe; } export interface SortTimeline { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx index 6d332c79f77cd..d2ddaae47d1e3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx @@ -124,7 +124,12 @@ export const StatefulOpenTimelineComponent = React.memo( defaultTimelineCount, templateTimelineCount, }); - const { timelineStatus, templateTimelineType, templateTimelineFilter } = useTimelineStatus({ + const { + timelineStatus, + templateTimelineType, + templateTimelineFilter, + installPrepackagedTimelines, + } = useTimelineStatus({ timelineType, customTemplateTimelineCount, elasticTemplateTimelineCount, @@ -287,7 +292,13 @@ export const StatefulOpenTimelineComponent = React.memo( focusInput(); }, []); - useEffect(() => refetch(), [refetch]); + useEffect(() => { + const fetchData = async () => { + await installPrepackagedTimelines(); + refetch(); + }; + fetchData(); + }, [refetch, installPrepackagedTimelines]); return !isModal ? ( void; } => { const [selectedTab, setSelectedTab] = useState( TemplateTimelineType.elastic @@ -101,9 +103,16 @@ export const useTimelineStatus = ({ : null; }, [templateTimelineType, filters, isTemplateFilterEnabled, onFilterClicked]); + const installPrepackagedTimelines = useCallback(async () => { + if (templateTimelineType === TemplateTimelineType.elastic) { + await installPrepackedTimelines(); + } + }, [templateTimelineType]); + return { timelineStatus, templateTimelineType, templateTimelineFilter, + installPrepackagedTimelines, }; }; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/api.ts b/x-pack/plugins/security_solution/public/timelines/containers/api.ts index 72e1f1d4de32d..e08d52066ebdc 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/api.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/api.ts @@ -14,6 +14,8 @@ import { TimelineStatus, TimelineErrorResponseType, TimelineErrorResponse, + ImportTimelineResultSchema, + importTimelineResultSchema, } from '../../../common/types/timeline'; import { TimelineInput, TimelineType } from '../../graphql/types'; import { @@ -21,6 +23,7 @@ import { TIMELINE_DRAFT_URL, TIMELINE_IMPORT_URL, TIMELINE_EXPORT_URL, + TIMELINE_PREPACKAGED_URL, } from '../../../common/constants'; import { KibanaServices } from '../../common/lib/kibana'; @@ -56,6 +59,12 @@ const decodeTimelineErrorResponse = (respTimeline?: TimelineErrorResponse) => fold(throwErrors(createToasterPlainError), identity) ); +const decodePrepackedTimelineResponse = (respTimeline?: ImportTimelineResultSchema) => + pipe( + importTimelineResultSchema.decode(respTimeline), + fold(throwErrors(createToasterPlainError), identity) + ); + const postTimeline = async ({ timeline }: RequestPostTimeline): Promise => { const response = await KibanaServices.get().http.post(TIMELINE_URL, { method: 'POST', @@ -200,3 +209,12 @@ export const cleanDraftTimeline = async ({ return decodeTimelineResponse(response); }; + +export const installPrepackedTimelines = async (): Promise => { + const response = await KibanaServices.get().http.post( + TIMELINE_PREPACKAGED_URL, + {} + ); + + return decodePrepackedTimelineResponse(response); +}; diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts index 15e188e281d10..7cbeea67b2750 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts @@ -183,8 +183,8 @@ export const timelineSchema = gql` } input PageInfoTimeline { - pageIndex: Float! - pageSize: Float! + pageIndex: Float + pageSize: Float } enum SortFieldTimeline { diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index 6553f709a7fa7..b44a8f5cceaf1 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -98,9 +98,9 @@ export interface TlsSortField { } export interface PageInfoTimeline { - pageIndex: number; + pageIndex?: Maybe; - pageSize: number; + pageSize?: Maybe; } export interface SortTimeline { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts index 0cec1832dab83..bfaab096a5013 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts @@ -26,3 +26,9 @@ export const createMockConfig = () => ({ to: 'now', }, }); + +export const mockGetCurrentUser = { + user: { + username: 'mockUser', + }, +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts index 945ce5ad85c79..3ce8d08a57ace 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts @@ -11,9 +11,11 @@ import { getEmptyIndex, getNonEmptyIndex, } from '../__mocks__/request_responses'; -import { requestContextMock, serverMock } from '../__mocks__'; -import { addPrepackedRulesRoute } from './add_prepackaged_rules_route'; +import { requestContextMock, serverMock, createMockConfig, mockGetCurrentUser } from '../__mocks__'; import { AddPrepackagedRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema'; +import { SecurityPluginSetup } from '../../../../../../security/server'; +import { installPrepackagedTimelines } from '../../../timeline/routes/utils/install_prepacked_timelines'; +import { addPrepackedRulesRoute } from './add_prepackaged_rules_route'; jest.mock('../../rules/get_prepackaged_rules', () => { return { @@ -51,18 +53,46 @@ jest.mock('../../rules/get_prepackaged_rules', () => { }; }); +jest.mock('../../../timeline/routes/utils/install_prepacked_timelines', () => { + return { + installPrepackagedTimelines: jest.fn().mockResolvedValue({ + success: true, + success_count: 3, + errors: [], + timelines_installed: 3, + timelines_updated: 0, + }), + }; +}); + describe('add_prepackaged_rules_route', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + let securitySetup: SecurityPluginSetup; beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); + securitySetup = ({ + authc: { + getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), + }, + authz: {}, + } as unknown) as SecurityPluginSetup; clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - addPrepackedRulesRoute(server.router); + (installPrepackagedTimelines as jest.Mock).mockReset(); + (installPrepackagedTimelines as jest.Mock).mockResolvedValue({ + success: true, + success_count: 0, + timelines_installed: 3, + timelines_updated: 0, + errors: [], + }); + + addPrepackedRulesRoute(server.router, createMockConfig(), securitySetup); }); describe('status codes', () => { @@ -120,6 +150,8 @@ describe('add_prepackaged_rules_route', () => { expect(response.body).toEqual({ rules_installed: 1, rules_updated: 0, + timelines_installed: 3, + timelines_updated: 0, }); }); @@ -131,6 +163,8 @@ describe('add_prepackaged_rules_route', () => { expect(response.body).toEqual({ rules_installed: 0, rules_updated: 1, + timelines_installed: 3, + timelines_updated: 0, }); }); @@ -145,4 +179,96 @@ describe('add_prepackaged_rules_route', () => { expect(response.body).toEqual({ message: 'Test error', status_code: 500 }); }); }); + + test('should install prepackaged timelines', async () => { + (installPrepackagedTimelines as jest.Mock).mockReset(); + (installPrepackagedTimelines as jest.Mock).mockResolvedValue({ + success: false, + success_count: 0, + timelines_installed: 0, + timelines_updated: 0, + errors: [ + { + id: '36429040-b529-11ea-8d8b-21de98be11a6', + error: { + message: 'timeline_id: "36429040-b529-11ea-8d8b-21de98be11a6" already exists', + status_code: 409, + }, + }, + ], + }); + const request = addPrepackagedRulesRequest(); + const response = await server.inject(request, context); + expect(response.body).toEqual({ + rules_installed: 0, + rules_updated: 1, + timelines_installed: 0, + timelines_updated: 0, + }); + }); + + test('should include the result of installing prepackaged timelines - timelines_installed', async () => { + (installPrepackagedTimelines as jest.Mock).mockReset(); + (installPrepackagedTimelines as jest.Mock).mockResolvedValue({ + success: true, + success_count: 1, + timelines_installed: 1, + timelines_updated: 0, + errors: [], + }); + const request = addPrepackagedRulesRequest(); + const response = await server.inject(request, context); + expect(response.body).toEqual({ + rules_installed: 0, + rules_updated: 1, + timelines_installed: 1, + timelines_updated: 0, + }); + }); + + test('should include the result of installing prepackaged timelines - timelines_updated', async () => { + (installPrepackagedTimelines as jest.Mock).mockReset(); + (installPrepackagedTimelines as jest.Mock).mockResolvedValue({ + success: true, + success_count: 1, + timelines_installed: 0, + timelines_updated: 1, + errors: [], + }); + const request = addPrepackagedRulesRequest(); + const response = await server.inject(request, context); + expect(response.body).toEqual({ + rules_installed: 0, + rules_updated: 1, + timelines_installed: 0, + timelines_updated: 1, + }); + }); + + test('should include the result of installing prepackaged timelines - skip the error message', async () => { + (installPrepackagedTimelines as jest.Mock).mockReset(); + (installPrepackagedTimelines as jest.Mock).mockResolvedValue({ + success: false, + success_count: 0, + timelines_installed: 0, + timelines_updated: 0, + errors: [ + { + id: '36429040-b529-11ea-8d8b-21de98be11a6', + error: { + message: 'timeline_id: "36429040-b529-11ea-8d8b-21de98be11a6" already exists', + status_code: 409, + }, + }, + ], + }); + const request = addPrepackagedRulesRequest(); + const response = await server.inject(request, context); + expect(response.body).toEqual({ + rules_installed: 0, + rules_updated: 1, + timelines_installed: 0, + timelines_updated: 0, + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index 9878521c49322..1226be71f63f5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -4,15 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ +import { IRouter } from '../../../../../../../../src/core/server'; + import { validate } from '../../../../../common/validate'; import { - PrePackagedRulesSchema, - prePackagedRulesSchema, + PrePackagedRulesAndTimelinesSchema, + prePackagedRulesAndTimelinesSchema, } from '../../../../../common/detection_engine/schemas/response/prepackaged_rules_schema'; -import { IRouter } from '../../../../../../../../src/core/server'; +import { importTimelineResultSchema } from '../../../../../common/types/timeline'; import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../../common/constants'; + +import { ConfigType } from '../../../../config'; +import { SetupPlugins } from '../../../../plugin'; +import { buildFrameworkRequest } from '../../../timeline/routes/utils/common'; +import { installPrepackagedTimelines } from '../../../timeline/routes/utils/install_prepacked_timelines'; + import { getIndexExists } from '../../index/get_index_exists'; -import { transformError, buildSiemResponse } from '../utils'; import { getPrepackagedRules } from '../../rules/get_prepackaged_rules'; import { installPrepackagedRules } from '../../rules/install_prepacked_rules'; import { updatePrepackagedRules } from '../../rules/update_prepacked_rules'; @@ -20,7 +27,13 @@ import { getRulesToInstall } from '../../rules/get_rules_to_install'; import { getRulesToUpdate } from '../../rules/get_rules_to_update'; import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules'; -export const addPrepackedRulesRoute = (router: IRouter) => { +import { transformError, buildSiemResponse } from '../utils'; + +export const addPrepackedRulesRoute = ( + router: IRouter, + config: ConfigType, + security: SetupPlugins['security'] +) => { router.put( { path: DETECTION_ENGINE_PREPACKAGED_URL, @@ -31,6 +44,7 @@ export const addPrepackedRulesRoute = (router: IRouter) => { }, async (context, _, response) => { const siemResponse = buildSiemResponse(response); + const frameworkRequest = await buildFrameworkRequest(context, security, _); try { const alertsClient = context.alerting?.getAlertsClient(); @@ -41,13 +55,10 @@ export const addPrepackedRulesRoute = (router: IRouter) => { if (!siemClient || !alertsClient) { return siemResponse.error({ statusCode: 404 }); } - const rulesFromFileSystem = getPrepackagedRules(); - const prepackagedRules = await getExistingPrepackagedRules({ alertsClient }); const rulesToInstall = getRulesToInstall(rulesFromFileSystem, prepackagedRules); const rulesToUpdate = getRulesToUpdate(rulesFromFileSystem, prepackagedRules); - const signalsIndex = siemClient.getSignalsIndex(); if (rulesToInstall.length !== 0 || rulesToUpdate.length !== 0) { const signalsIndexExists = await getIndexExists( @@ -61,15 +72,31 @@ export const addPrepackedRulesRoute = (router: IRouter) => { }); } } - await Promise.all(installPrepackagedRules(alertsClient, rulesToInstall, signalsIndex)); + const result = await Promise.all([ + installPrepackagedRules(alertsClient, rulesToInstall, signalsIndex), + installPrepackagedTimelines(config.maxTimelineImportExportSize, frameworkRequest, true), + ]); + const [prepackagedTimelinesResult, timelinesErrors] = validate( + result[1], + importTimelineResultSchema + ); await updatePrepackagedRules(alertsClient, savedObjectsClient, rulesToUpdate, signalsIndex); - const prepackagedRulesOutput: PrePackagedRulesSchema = { + + const prepackagedRulesOutput: PrePackagedRulesAndTimelinesSchema = { rules_installed: rulesToInstall.length, rules_updated: rulesToUpdate.length, + timelines_installed: prepackagedTimelinesResult?.timelines_installed ?? 0, + timelines_updated: prepackagedTimelinesResult?.timelines_updated ?? 0, }; - const [validated, errors] = validate(prepackagedRulesOutput, prePackagedRulesSchema); - if (errors != null) { - return siemResponse.error({ statusCode: 500, body: errors }); + const [validated, genericErrors] = validate( + prepackagedRulesOutput, + prePackagedRulesAndTimelinesSchema + ); + if (genericErrors != null && timelinesErrors != null) { + return siemResponse.error({ + statusCode: 500, + body: [genericErrors, timelinesErrors].filter((msg) => msg != null).join(', '), + }); } else { return response.ok({ body: validated ?? {} }); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts index 03059ed5ec5cc..f8b6f7e3ddcba 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts @@ -12,7 +12,8 @@ import { getPrepackagedRulesStatusRequest, getNonEmptyIndex, } from '../__mocks__/request_responses'; -import { requestContextMock, serverMock } from '../__mocks__'; +import { requestContextMock, serverMock, createMockConfig } from '../__mocks__'; +import { SecurityPluginSetup } from '../../../../../../security/server'; jest.mock('../../rules/get_prepackaged_rules', () => { return { @@ -38,17 +39,31 @@ jest.mock('../../rules/get_prepackaged_rules', () => { }); describe('get_prepackaged_rule_status_route', () => { + const mockGetCurrentUser = { + user: { + username: 'mockUser', + }, + }; + let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + let securitySetup: SecurityPluginSetup; beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); + securitySetup = ({ + authc: { + getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), + }, + authz: {}, + } as unknown) as SecurityPluginSetup; + clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); - getPrepackagedRulesStatusRoute(server.router); + getPrepackagedRulesStatusRoute(server.router, createMockConfig(), securitySetup); }); describe('status codes with actionClient and alertClient', () => { @@ -89,6 +104,9 @@ describe('get_prepackaged_rule_status_route', () => { rules_installed: 0, rules_not_installed: 1, rules_not_updated: 0, + timelines_installed: 0, + timelines_not_installed: 0, + timelines_not_updated: 0, }); }); @@ -103,6 +121,9 @@ describe('get_prepackaged_rule_status_route', () => { rules_installed: 1, rules_not_installed: 0, rules_not_updated: 1, + timelines_installed: 0, + timelines_not_installed: 0, + timelines_not_updated: 0, }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts index bc199ee132e96..4cd5238ccb1ef 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts @@ -6,8 +6,8 @@ import { validate } from '../../../../../common/validate'; import { - PrePackagedRulesStatusSchema, - prePackagedRulesStatusSchema, + PrePackagedRulesAndTimelinesStatusSchema, + prePackagedRulesAndTimelinesStatusSchema, } from '../../../../../common/detection_engine/schemas/response/prepackaged_rules_status_schema'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../../common/constants'; @@ -17,8 +17,17 @@ import { getRulesToInstall } from '../../rules/get_rules_to_install'; import { getRulesToUpdate } from '../../rules/get_rules_to_update'; import { findRules } from '../../rules/find_rules'; import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules'; +import { buildFrameworkRequest } from '../../../timeline/routes/utils/common'; +import { ConfigType } from '../../../../config'; +import { SetupPlugins } from '../../../../plugin'; +import { checkTimelinesStatus } from '../../../timeline/routes/utils/check_timelines_status'; +import { checkTimelineStatusRt } from '../../../timeline/routes/schemas/check_timelines_status_schema'; -export const getPrepackagedRulesStatusRoute = (router: IRouter) => { +export const getPrepackagedRulesStatusRoute = ( + router: IRouter, + config: ConfigType, + security: SetupPlugins['security'] +) => { router.get( { path: `${DETECTION_ENGINE_PREPACKAGED_URL}/_status`, @@ -46,16 +55,31 @@ export const getPrepackagedRulesStatusRoute = (router: IRouter) => { filter: 'alert.attributes.tags:"__internal_immutable:false"', fields: undefined, }); + const frameworkRequest = await buildFrameworkRequest(context, security, request); const prepackagedRules = await getExistingPrepackagedRules({ alertsClient }); + const rulesToInstall = getRulesToInstall(rulesFromFileSystem, prepackagedRules); const rulesToUpdate = getRulesToUpdate(rulesFromFileSystem, prepackagedRules); - const prepackagedRulesStatus: PrePackagedRulesStatusSchema = { + const prepackagedTimelineStatus = await checkTimelinesStatus(frameworkRequest); + const [validatedprepackagedTimelineStatus] = validate( + prepackagedTimelineStatus, + checkTimelineStatusRt + ); + + const prepackagedRulesStatus: PrePackagedRulesAndTimelinesStatusSchema = { rules_custom_installed: customRules.total, rules_installed: prepackagedRules.length, rules_not_installed: rulesToInstall.length, rules_not_updated: rulesToUpdate.length, + timelines_installed: validatedprepackagedTimelineStatus?.prepackagedTimelines.length ?? 0, + timelines_not_installed: + validatedprepackagedTimelineStatus?.timelinesToInstall.length ?? 0, + timelines_not_updated: validatedprepackagedTimelineStatus?.timelinesToUpdate.length ?? 0, }; - const [validated, errors] = validate(prepackagedRulesStatus, prePackagedRulesStatusSchema); + const [validated, errors] = validate( + prepackagedRulesStatus, + prePackagedRulesAndTimelinesStatusSchema + ); if (errors != null) { return siemResponse.error({ statusCode: 500, body: errors }); } else { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/README.md b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/README.md new file mode 100644 index 0000000000000..901dacbfe80cc --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/README.md @@ -0,0 +1,182 @@ + + +### How to on board a new prepackage timelines: + + + +1. [Have the env params set up](https://github.com/elastic/kibana/blob/master/x-pack/plugins/siem/server/lib/detection_engine/README.md) + +2. Create a new timelines template into `x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines` + + ##### 2.a : Create a new template from UI and export it. + + 1. Go to Security Solution app in Kibana + 2. Go to timelines > templates > custom templates (a filter on the right) + 3. Click `Create new timeline template` + 4. Edit your template + 5. Export only **one** timeline template each time and put that in `x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines`. (For potential update requirement in the future, we put one timeline in each file to keep nice and clear) + 6. Rename the file extension to `.json` + 7. Check the chapter of `Fields to hightlight for on boarding a new prepackaged timeline` in this readme and update your template + + + + + ##### 2.b : Create a new template from scratch + Please note that below template is just an example, please replace all your fields with whatever makes sense. Do check `Fields to hightlight for on boarding a new prepackaged timeline` to make sure the template can be created as expected. + + + cd x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines + + + + echo '{"savedObjectId":null,"version":null,"columns":[{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"@timestamp","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"signal.rule.description","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"event.action","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.name","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"The working directory of the process.","columnHeaderType":"not-filtered","id":"process.working_directory","category":"process","type":"string","searchable":null,"example":"/home/alice"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.args","category":"process","type":"string","searchable":null,"example":"[\"/usr/bin/ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.pid","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Absolute path to the process executable.","columnHeaderType":"not-filtered","id":"process.parent.executable","category":"process","type":"string","searchable":null,"example":"/usr/bin/ssh"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.parent.args","category":"process","type":"string","searchable":null,"example":"[\"ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"aggregatable":true,"name":null,"description":"Process id.","columnHeaderType":"not-filtered","id":"process.parent.pid","category":"process","type":"number","searchable":null,"example":"4242"},{"indexes":null,"aggregatable":true,"name":null,"description":"Short name or login of the user.","columnHeaderType":"not-filtered","id":"user.name","category":"user","type":"string","searchable":null,"example":"albert"},{"indexes":null,"aggregatable":true,"name":null,"description":"Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.","columnHeaderType":"not-filtered","id":"host.name","category":"host","type":"string","searchable":null}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde","queryMatch":{"displayValue":null,"field":"_id","displayField":null,"value":"590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde","operator":":"},"id":"send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"kind":"kuery","expression":""},"serializedQuery":""}},"title":"Generic Process Timeline","dateRange":{"start":1588161020848,"end":1588162280848},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"created":1588162404153,"createdBy":"Elastic","updated":1588604767818,"updatedBy":"Elastic","eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"timelineType":"template","status":"immutable","templateTimelineId":"2c7e0663-5a91-0004-aa15-26bf756d2c40","templateTimelineVersion":1}' > my_new_template.json``` + + #### Note that the json has to be minified. + #### Fields to hightlight for on boarding a new prepackaged timeline: + + - savedObjectId: null + + - version: null + + - templateTimelineId: Specify an unique uuid e.g.: `2c7e0663-5a91-0004-aa15-26bf756d2c40` + + - templateTimelineVersion: just start from `1` + + - timelineType: `template` + + - status: `immutable` + + + +3. ```cd x-pack/plugins/security_solution/server/lib/detection_engine/scripts``` + +4. ```sh ./timelines/regen_prepackage_timelines_index.sh``` + +(this will update `x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/index.ndjson`) + + + +5. Go to `http://localhost:5601/app/security#/detections/rules` and click on `Install Elastic prebuild rules` + +or run + +``` +cd x-pack/plugins/security_solution/server/lib/detection_engine/scripts + +sh ./timelines/add_prepackaged_timelines.sh + +``` + + + +6. Check in UI or run the script below to see if prepackaged timelines on-boarded correctly. + +``` + +sh ./timelines/find_timeline_by_filter.sh immutable template elastic + +``` + + + +### How to update an existing prepackage timeline: + +1. ```cd x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines``` + +2. Open the json file you wish to update, and remember to bump the `templateTimelineVersion` + +3. Go to ```cd x-pack/plugins/security_solution/server/lib/detection_engine/scripts```, run ```sh ./timelines/regen_prepackage_timelines_index.sh``` + +4. Go to `http://localhost:5601/app/security#/detections/rules` and click on `Install Elastic prebuild rules` + +or run + +``` + +sh ./timelines/add_prepackaged_timelines.sh + +``` + + + +5. Check in UI or run the script below to see if the prepackaged timeline updated correctly. + +``` + +sh ./timelines/find_timeline_by_filter.sh immutable template elastic + +``` + + + + +### How to install prepackaged timelines: + +1. ```cd x-pack/plugins/siem/server/lib/detection_engine/scripts``` + +2. ```sh ./timelines/add_prepackaged_timelines.sh``` + +3. ```sh ./timelines/find_timeline_by_filter.sh immutable template elastic``` + + + +### Get timeline by id: + +``` + +cd x-pack/plugins/security_solution/server/lib/detection_engine/scripts + +sh ./timelines/get_timeline_by_id.sh {id} + +``` + + + + +### Get timeline by templateTimelineId: + +``` + +cd x-pack/plugins/security_solution/server/lib/detection_engine/scripts + +sh ./timelines/get_timeline_by_template_timeline_id.sh {template_timeline_id} + +``` + + + + +### Get all custom timelines: + +``` + +cd x-pack/plugins/security_solution/server/lib/detection_engine/scripts + +sh ./timelines/get_all_timelines.sh + +``` + + + + +### Delete all timelines: + +``` + +cd x-pack/plugins/security_solution/server/lib/detection_engine/scripts + +sh ./timelines/delete_all_timelines.sh + +``` + + + +### Delete timeline by timeline id: + +``` + +cd x-pack/plugins/security_solution/server/lib/detection_engine/scripts + +./timelines/delete_all_alerts.sh {timeline_id} + +``` diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/endpoint.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/endpoint.json new file mode 100644 index 0000000000000..711050e1f136a --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/endpoint.json @@ -0,0 +1 @@ +{"savedObjectId":null,"version":null,"columns":[{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"@timestamp","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"signal.rule.description","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"event.action","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.name","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"The working directory of the process.","columnHeaderType":"not-filtered","id":"process.working_directory","category":"process","type":"string","searchable":null,"example":"/home/alice"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.args","category":"process","type":"string","searchable":null,"example":"[\"/usr/bin/ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.pid","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Absolute path to the process executable.","columnHeaderType":"not-filtered","id":"process.parent.executable","category":"process","type":"string","searchable":null,"example":"/usr/bin/ssh"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.parent.args","category":"process","type":"string","searchable":null,"example":"[\"ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"aggregatable":true,"name":null,"description":"Process id.","columnHeaderType":"not-filtered","id":"process.parent.pid","category":"process","type":"number","searchable":null,"example":"4242"},{"indexes":null,"aggregatable":true,"name":null,"description":"Short name or login of the user.","columnHeaderType":"not-filtered","id":"user.name","category":"user","type":"string","searchable":null,"example":"albert"},{"indexes":null,"aggregatable":true,"name":null,"description":"Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.","columnHeaderType":"not-filtered","id":"host.name","category":"host","type":"string","searchable":null}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"{process.name}","queryMatch":{"displayValue":"{agent.type}","field":"agent.type","displayField":"agent.type","value":"{agent.type}","operator":":*"},"id":"timeline-1-8622010a-61fb-490d-b162-beac9c36a853","type":"template","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"","queryMatch":{"displayValue":"endpoint","field":"agent.type","displayField":"agent.type","value":"endpoint","operator":":"},"id":"timeline-1-4685da24-35c1-43f3-892d-1f926dbf5568","type":"default","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"kind":"kuery","expression":""},"serializedQuery":""}},"title":"Generic Endpoint Timeline","timelineType":"template","templateTimelineVersion":1,"templateTimelineId":"db366523-f1c6-4c1f-8731-6ce5ed9e5717","dateRange":{"start":1588161020848,"end":1588162280848},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"created":1594735857110,"createdBy":"Elastic","updated":1594736314036,"updatedBy":"Elastic","eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/index.ndjson b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/index.ndjson new file mode 100644 index 0000000000000..7c074242c39d1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/index.ndjson @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// Auto generated file from scripts/regen_prepackage_timelines_index.sh +// Do not hand edit. Run that script to regenerate package information instead + +{"savedObjectId":null,"version":null,"columns":[{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"@timestamp","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"signal.rule.description","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"event.action","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.name","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"The working directory of the process.","columnHeaderType":"not-filtered","id":"process.working_directory","category":"process","type":"string","searchable":null,"example":"/home/alice"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.args","category":"process","type":"string","searchable":null,"example":"[\"/usr/bin/ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.pid","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Absolute path to the process executable.","columnHeaderType":"not-filtered","id":"process.parent.executable","category":"process","type":"string","searchable":null,"example":"/usr/bin/ssh"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.parent.args","category":"process","type":"string","searchable":null,"example":"[\"ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"aggregatable":true,"name":null,"description":"Process id.","columnHeaderType":"not-filtered","id":"process.parent.pid","category":"process","type":"number","searchable":null,"example":"4242"},{"indexes":null,"aggregatable":true,"name":null,"description":"Short name or login of the user.","columnHeaderType":"not-filtered","id":"user.name","category":"user","type":"string","searchable":null,"example":"albert"},{"indexes":null,"aggregatable":true,"name":null,"description":"Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.","columnHeaderType":"not-filtered","id":"host.name","category":"host","type":"string","searchable":null}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"{process.name}","queryMatch":{"displayValue":"{agent.type}","field":"agent.type","displayField":"agent.type","value":"{agent.type}","operator":":*"},"id":"timeline-1-8622010a-61fb-490d-b162-beac9c36a853","type":"template","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"","queryMatch":{"displayValue":"endpoint","field":"agent.type","displayField":"agent.type","value":"endpoint","operator":":"},"id":"timeline-1-4685da24-35c1-43f3-892d-1f926dbf5568","type":"default","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"kind":"kuery","expression":""},"serializedQuery":""}},"title":"Generic Endpoint Timeline","timelineType":"template","templateTimelineVersion":1,"templateTimelineId":"db366523-f1c6-4c1f-8731-6ce5ed9e5717","dateRange":{"start":1588161020848,"end":1588162280848},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"created":1594735857110,"createdBy":"Elastic","updated":1594736314036,"updatedBy":"Elastic","eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} +{"savedObjectId":null,"version":null,"columns":[{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"@timestamp","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"signal.rule.description","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"The action captured by the event.\n\nThis describes the information in the event. It is more specific than `event.category`.\nExamples are `group-add`, `process-started`, `file-created`. The value is\nnormally defined by the implementer.","columnHeaderType":"not-filtered","id":"event.action","category":"event","type":"string","searchable":null,"example":"user-password-change"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.args","category":"process","type":"string","searchable":null,"example":"[\"/usr/bin/ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.pid","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"IP address of the source (IPv4 or IPv6).","columnHeaderType":"not-filtered","id":"source.ip","category":"source","type":"ip","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Port of the source.","columnHeaderType":"not-filtered","id":"source.port","category":"source","type":"number","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"IP address of the destination (IPv4 or IPv6).","columnHeaderType":"not-filtered","id":"destination.ip","category":"destination","type":"ip","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"destination.port","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Short name or login of the user.","columnHeaderType":"not-filtered","id":"user.name","category":"user","type":"string","searchable":null,"example":"albert"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"host.name","searchable":null}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"{source.ip}","queryMatch":{"displayValue":null,"field":"source.ip","displayField":null,"value":"{source.ip}","operator":":*"},"id":"timeline-1-ec778f01-1802-40f0-9dfb-ed8de1f656cb","type":"template","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"{destination.ip}","queryMatch":{"displayValue":null,"field":"destination.ip","displayField":null,"value":"{destination.ip}","operator":":*"},"id":"timeline-1-e37e37c5-a6e7-4338-af30-47bfbc3c0e1e","type":"template","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"kind":"kuery","expression":""},"serializedQuery":""}},"title":"Generic Network Timeline","timelineType":"template","templateTimelineVersion":1,"templateTimelineId":"91832785-286d-4ebe-b884-1a208d111a70","dateRange":{"start":1588255858373,"end":1588256218373},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"created":1594735573866,"createdBy":"Elastic","updated":1594736099397,"updatedBy":"Elastic","eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} +{"savedObjectId":null,"version":null,"columns":[{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"@timestamp","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"signal.rule.description","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"event.action","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.name","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"The working directory of the process.","columnHeaderType":"not-filtered","id":"process.working_directory","category":"process","type":"string","searchable":null,"example":"/home/alice"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.args","category":"process","type":"string","searchable":null,"example":"[\"/usr/bin/ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.pid","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Absolute path to the process executable.","columnHeaderType":"not-filtered","id":"process.parent.executable","category":"process","type":"string","searchable":null,"example":"/usr/bin/ssh"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.parent.args","category":"process","type":"string","searchable":null,"example":"[\"ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"aggregatable":true,"name":null,"description":"Process id.","columnHeaderType":"not-filtered","id":"process.parent.pid","category":"process","type":"number","searchable":null,"example":"4242"},{"indexes":null,"aggregatable":true,"name":null,"description":"Short name or login of the user.","columnHeaderType":"not-filtered","id":"user.name","category":"user","type":"string","searchable":null,"example":"albert"},{"indexes":null,"aggregatable":true,"name":null,"description":"Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.","columnHeaderType":"not-filtered","id":"host.name","category":"host","type":"string","searchable":null}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"{process.name}","queryMatch":{"displayValue":null,"field":"process.name","displayField":null,"value":"{process.name}","operator":":"},"id":"timeline-1-8622010a-61fb-490d-b162-beac9c36a853","type":"template","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"{event.type}","queryMatch":{"displayValue":null,"field":"event.type","displayField":null,"value":"{event.type}","operator":":*"},"id":"timeline-1-4685da24-35c1-43f3-892d-1f926dbf5568","type":"template","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"kind":"kuery","expression":""},"serializedQuery":""}},"title":"Generic Process Timeline","timelineType":"template","templateTimelineVersion":1,"templateTimelineId":"76e52245-7519-4251-91ab-262fb1a1728c","dateRange":{"start":1588161020848,"end":1588162280848},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"created":1594735629389,"createdBy":"Elastic","updated":1594736083598,"updatedBy":"Elastic","eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/network.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/network.json new file mode 100644 index 0000000000000..1634476b4e99d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/network.json @@ -0,0 +1 @@ +{"savedObjectId":null,"version":null,"columns":[{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"@timestamp","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"signal.rule.description","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"The action captured by the event.\n\nThis describes the information in the event. It is more specific than `event.category`.\nExamples are `group-add`, `process-started`, `file-created`. The value is\nnormally defined by the implementer.","columnHeaderType":"not-filtered","id":"event.action","category":"event","type":"string","searchable":null,"example":"user-password-change"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.args","category":"process","type":"string","searchable":null,"example":"[\"/usr/bin/ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.pid","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"IP address of the source (IPv4 or IPv6).","columnHeaderType":"not-filtered","id":"source.ip","category":"source","type":"ip","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Port of the source.","columnHeaderType":"not-filtered","id":"source.port","category":"source","type":"number","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"IP address of the destination (IPv4 or IPv6).","columnHeaderType":"not-filtered","id":"destination.ip","category":"destination","type":"ip","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"destination.port","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Short name or login of the user.","columnHeaderType":"not-filtered","id":"user.name","category":"user","type":"string","searchable":null,"example":"albert"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"host.name","searchable":null}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"{source.ip}","queryMatch":{"displayValue":null,"field":"source.ip","displayField":null,"value":"{source.ip}","operator":":*"},"id":"timeline-1-ec778f01-1802-40f0-9dfb-ed8de1f656cb","type":"template","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"{destination.ip}","queryMatch":{"displayValue":null,"field":"destination.ip","displayField":null,"value":"{destination.ip}","operator":":*"},"id":"timeline-1-e37e37c5-a6e7-4338-af30-47bfbc3c0e1e","type":"template","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"kind":"kuery","expression":""},"serializedQuery":""}},"title":"Generic Network Timeline","timelineType":"template","templateTimelineVersion":1,"templateTimelineId":"91832785-286d-4ebe-b884-1a208d111a70","dateRange":{"start":1588255858373,"end":1588256218373},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"created":1594735573866,"createdBy":"Elastic","updated":1594736099397,"updatedBy":"Elastic","eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/process.json new file mode 100644 index 0000000000000..767f38133f263 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/process.json @@ -0,0 +1 @@ +{"savedObjectId":null,"version":null,"columns":[{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"@timestamp","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"signal.rule.description","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"event.action","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.name","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"The working directory of the process.","columnHeaderType":"not-filtered","id":"process.working_directory","category":"process","type":"string","searchable":null,"example":"/home/alice"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.args","category":"process","type":"string","searchable":null,"example":"[\"/usr/bin/ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.pid","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Absolute path to the process executable.","columnHeaderType":"not-filtered","id":"process.parent.executable","category":"process","type":"string","searchable":null,"example":"/usr/bin/ssh"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.parent.args","category":"process","type":"string","searchable":null,"example":"[\"ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"aggregatable":true,"name":null,"description":"Process id.","columnHeaderType":"not-filtered","id":"process.parent.pid","category":"process","type":"number","searchable":null,"example":"4242"},{"indexes":null,"aggregatable":true,"name":null,"description":"Short name or login of the user.","columnHeaderType":"not-filtered","id":"user.name","category":"user","type":"string","searchable":null,"example":"albert"},{"indexes":null,"aggregatable":true,"name":null,"description":"Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.","columnHeaderType":"not-filtered","id":"host.name","category":"host","type":"string","searchable":null}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"{process.name}","queryMatch":{"displayValue":null,"field":"process.name","displayField":null,"value":"{process.name}","operator":":"},"id":"timeline-1-8622010a-61fb-490d-b162-beac9c36a853","type":"template","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"{event.type}","queryMatch":{"displayValue":null,"field":"event.type","displayField":null,"value":"{event.type}","operator":":*"},"id":"timeline-1-4685da24-35c1-43f3-892d-1f926dbf5568","type":"template","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"kind":"kuery","expression":""},"serializedQuery":""}},"title":"Generic Process Timeline","timelineType":"template","templateTimelineVersion":1,"templateTimelineId":"76e52245-7519-4251-91ab-262fb1a1728c","dateRange":{"start":1588161020848,"end":1588162280848},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"created":1594735629389,"createdBy":"Elastic","updated":1594736083598,"updatedBy":"Elastic","eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/export_timelines_to_file.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/export_timelines_to_file.sh new file mode 100644 index 0000000000000..92799e575043b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/export_timelines_to_file.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +FILENAME=${1:-test_timeline.ndjson} + +# Example export to the file named test_timeline.ndjson +# ./export_timelines_to_file.sh + +# Example export to the file named my_test_timeline.ndjson +# ./export_timelines_to_file.sh my_test_timeline.ndjson +curl -s -k -OJ \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST "${KIBANA_URL}${SPACE_URL}/api/timeline/_export?file_name=${FILENAME}" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/regen_prepackge_rules_index.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/regen_prepackage_rules_index.sh similarity index 96% rename from x-pack/plugins/security_solution/server/lib/detection_engine/scripts/regen_prepackge_rules_index.sh rename to x-pack/plugins/security_solution/server/lib/detection_engine/scripts/regen_prepackage_rules_index.sh index 3bcf158703c7d..984d12bb740f1 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/regen_prepackge_rules_index.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/regen_prepackage_rules_index.sh @@ -30,4 +30,4 @@ for f in ../rules/prepackaged_rules/*.json ; do RULE_NUMBER=$[$RULE_NUMBER +1] done -echo "];" >> ${PREPACKAGED_RULES_INDEX} \ No newline at end of file +echo "];" >> ${PREPACKAGED_RULES_INDEX} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/add_prepackaged_timelines.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/add_prepackaged_timelines.sh new file mode 100755 index 0000000000000..5214552ea082a --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/add_prepackaged_timelines.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Uses a defaults if no argument is specified +TIMELINES=${1:-../../rules/prepackaged_timelines/index.ndjson} + +# Example to import and overwrite everything from ../rules/prepackaged_timelines/index.ndjson +# ./timelines/add_prepackaged_timelines.sh +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST "${KIBANA_URL}${SPACE_URL}/api/timeline/_prepackaged" \ + | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/delete_all_timelines.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/delete_all_timelines.sh new file mode 100755 index 0000000000000..b0cda7476da0c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/delete_all_timelines.sh @@ -0,0 +1,28 @@ + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./timelines/delete_all_timelines.sh +curl -s -k \ + -H "Content-Type: application/json" \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${ELASTICSEARCH_URL}/${KIBANA_INDEX}*/_delete_by_query \ + --data '{ + "query" : { + "bool": { + "minimum_should_match": 1, + "should": [ + {"exists" :{ "field": "siem-ui-timeline" }}, + {"exists" :{ "field": "siem-ui-timeline-note" }}, + {"exists" :{ "field": "siem-ui-timeline-pinned-event" }} + ] + } + } +}' \ +| jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/delete_timeline_by_timeline_id.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/delete_timeline_by_timeline_id.sh new file mode 100755 index 0000000000000..b1e9fbdd93841 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/delete_timeline_by_timeline_id.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + + +# Example: ./timelines/delete_timeline_by_id.sh {timeline_id} + +curl -s -k \ + -H "Content-Type: application/json" \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST "${KIBANA_URL}${SPACE_URL}/api/solutions/security/graphql" \ + -d '{"operationName":"DeleteTimelineMutation","variables":{"id":["'$1'"]},"query":"mutation DeleteTimelineMutation($id: [ID!]!) {\n deleteTimeline(id: $id)\n}\n"}' + diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/find_timeline_by_filter.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/find_timeline_by_filter.sh new file mode 100755 index 0000000000000..c267b4d9f36d5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/find_timeline_by_filter.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +STATUS=${1:-active} +TIMELINE_TYPE=${2:-default} +TEMPLATE_TIMELINE_TYPE=${3:-custom} + +# Example get all timelines: +# ./timelines/find_timeline_by_filter.sh active + +# Example get all prepackaged timeline templates: +# ./timelines/find_timeline_by_filter.sh immutable template elastic + +# Example get all custom timeline templates: +# ./timelines/find_timeline_by_filter.sh active template custom + +curl -s -k \ + -H "Content-Type: application/json" \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST "${KIBANA_URL}${SPACE_URL}/api/solutions/security/graphql" \ + -d '{"operationName":"GetAllTimeline","variables":{"onlyUserFavorite":false,"pageInfo":{"pageIndex":1,"pageSize":10},"search":"","sort":{"sortField":"updated","sortOrder":"desc"},"status":"'$STATUS'","timelineType":"'$TIMELINE_TYPE'","templateTimelineType":"'$TEMPLATE_TIMELINE_TYPE'"},"query":"query GetAllTimeline($pageInfo: PageInfoTimeline!, $search: String, $sort: SortTimeline, $onlyUserFavorite: Boolean, $timelineType: TimelineType, $templateTimelineType: TemplateTimelineType, $status: TimelineStatus) {\n getAllTimeline(pageInfo: $pageInfo, search: $search, sort: $sort, onlyUserFavorite: $onlyUserFavorite, timelineType: $timelineType, templateTimelineType: $templateTimelineType, status: $status) {\n totalCount\n defaultTimelineCount\n templateTimelineCount\n elasticTemplateTimelineCount\n customTemplateTimelineCount\n favoriteCount\n timeline {\n savedObjectId\n description\n favorite {\n fullName\n userName\n favoriteDate\n __typename\n }\n eventIdToNoteIds {\n eventId\n note\n timelineId\n noteId\n created\n createdBy\n timelineVersion\n updated\n updatedBy\n version\n __typename\n }\n notes {\n eventId\n note\n timelineId\n timelineVersion\n noteId\n created\n createdBy\n updated\n updatedBy\n version\n __typename\n }\n noteIds\n pinnedEventIds\n status\n title\n timelineType\n templateTimelineId\n templateTimelineVersion\n created\n createdBy\n updated\n updatedBy\n version\n __typename\n }\n __typename\n }\n}\n"}' \ + | jq . + + diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_all_timelines.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_all_timelines.sh new file mode 100755 index 0000000000000..f58632c7cbbe3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_all_timelines.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./timelines/get_all_timelines.sh +curl -s -k \ + -H "Content-Type: application/json" \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST "${KIBANA_URL}${SPACE_URL}/api/solutions/security/graphql" \ + -d '{ + "operationName": "GetAllTimeline", + "variables": { + "onlyUserFavorite": false, + "pageInfo": { + "pageIndex": null, + "pageSize": null + }, + "search": "", + "sort": { + "sortField": "updated", + "sortOrder": "desc" + }, + "status": "active", + "timelineType": null, + "templateTimelineType": null + }, + "query": "query GetAllTimeline($pageInfo: PageInfoTimeline!, $search: String, $sort: SortTimeline, $onlyUserFavorite: Boolean, $timelineType: TimelineType, $templateTimelineType: TemplateTimelineType, $status: TimelineStatus) {\n getAllTimeline(pageInfo: $pageInfo, search: $search, sort: $sort, onlyUserFavorite: $onlyUserFavorite, timelineType: $timelineType, templateTimelineType: $templateTimelineType, status: $status) {\n totalCount\n defaultTimelineCount\n templateTimelineCount\n elasticTemplateTimelineCount\n customTemplateTimelineCount\n favoriteCount\n timeline {\n savedObjectId\n description\n favorite {\n fullName\n userName\n favoriteDate\n __typename\n }\n eventIdToNoteIds {\n eventId\n note\n timelineId\n noteId\n created\n createdBy\n timelineVersion\n updated\n updatedBy\n version\n __typename\n }\n notes {\n eventId\n note\n timelineId\n timelineVersion\n noteId\n created\n createdBy\n updated\n updatedBy\n version\n __typename\n }\n noteIds\n pinnedEventIds\n status\n title\n timelineType\n templateTimelineId\n templateTimelineVersion\n created\n createdBy\n updated\n updatedBy\n version\n __typename\n }\n __typename\n }\n}\n" +}' | jq . + diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_id.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_id.sh new file mode 100755 index 0000000000000..0c0694c0591f9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_id.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./timelines/get_timeline_by_id.sh {timeline_id} + +curl -s -k \ + -H "Content-Type: application/json" \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/timeline?id=$1" \ + | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_template_timeline_id.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_template_timeline_id.sh new file mode 100755 index 0000000000000..36862b519130b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_template_timeline_id.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./timelines/get_timeline_by_template_timeline_id.sh {template_timeline_id} + +curl -s -k \ + -H "Content-Type: application/json" \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/timeline?template_timeline_id=$1" \ + | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/regen_prepackage_timelines_index.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/regen_prepackage_timelines_index.sh new file mode 100755 index 0000000000000..19c53a3c00b6d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/regen_prepackage_timelines_index.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +set -e +./check_env_variables.sh + +# Regenerates the index.ts that contains all of the timelines that are read in from json +PREPACKAGED_TIMELINES_INDEX=../rules/prepackaged_timelines/index.ndjson + +# Clear existing content +echo "" > ${PREPACKAGED_TIMELINES_INDEX} + +echo "/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// Auto generated file from scripts/regen_prepackage_timelines_index.sh +// Do not hand edit. Run that script to regenerate package information instead +" > ${PREPACKAGED_TIMELINES_INDEX} + +for f in ../rules/prepackaged_timelines/*.json ; do + echo "converting $f" + sed ':a;N;$!ba;s/\n/ /g' $f >> ${PREPACKAGED_TIMELINES_INDEX} +done diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts index adfdf831f22cf..2afe3197d6d64 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts @@ -162,7 +162,7 @@ export const mockUniqueParsedTemplateTimelineObjects = [ ]; export const mockParsedTemplateTimelineObjects = [ - { ...mockParsedObjects[0], ...mockGetTemplateTimelineValue }, + { ...mockParsedObjects[0], ...mockGetTemplateTimelineValue, templateTimelineVersion: 2 }, ]; export const mockGetDraftTimelineValue = { @@ -252,3 +252,949 @@ export const mockCreatedTemplateTimeline = { ...mockCreatedTimeline, ...mockGetTemplateTimelineValue, }; + +export const mockCheckTimelinesStatusBeforeInstallResult = { + timelinesToInstall: [ + { + savedObjectId: null, + version: null, + columns: [ + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: '@timestamp', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'signal.rule.description', + searchable: null, + }, + { + indexes: null, + aggregatable: true, + name: null, + description: + 'The action captured by the event.\n\nThis describes the information in the event. It is more specific than `event.category`.\nExamples are `group-add`, `process-started`, `file-created`. The value is\nnormally defined by the implementer.', + columnHeaderType: 'not-filtered', + id: 'event.action', + category: 'event', + type: 'string', + searchable: null, + example: 'user-password-change', + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'endgame.data.rule_name', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'rule.reference', + searchable: null, + }, + { + aggregatable: true, + description: + 'Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.', + columnHeaderType: 'not-filtered', + id: 'host.name', + category: 'host', + type: 'string', + }, + { + aggregatable: true, + description: 'Operating system name, without the version.', + columnHeaderType: 'not-filtered', + id: 'host.os.name', + category: 'host', + type: 'string', + example: 'Mac OS X', + }, + ], + dataProviders: [ + { + excluded: false, + and: [], + kqlQuery: '', + name: '3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509', + queryMatch: { + displayValue: null, + field: '_id', + displayField: null, + value: '3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509', + operator: ':', + }, + id: + 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509', + enabled: true, + }, + ], + description: '', + eventType: 'all', + filters: [], + kqlMode: 'filter', + kqlQuery: { + filterQuery: { + kuery: { + kind: 'kuery', + expression: '', + }, + serializedQuery: '', + }, + }, + title: 'Generic Endpoint Timeline', + dateRange: { + start: 1588257731065, + end: 1588258391065, + }, + savedQueryId: null, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + created: 1588258576517, + createdBy: 'Elastic', + updated: 1588261039030, + updatedBy: 'Elastic', + eventNotes: [], + globalNotes: [], + pinnedEventIds: [], + timelineType: 'template', + status: 'immutable', + templateTimelineId: '2c7e0663-5a91-0001-aa15-26bf756d2c39', + templateTimelineVersion: 1, + }, + { + savedObjectId: null, + version: null, + columns: [ + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: '@timestamp', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'signal.rule.description', + searchable: null, + }, + { + indexes: null, + aggregatable: true, + name: null, + description: + 'The action captured by the event.\n\nThis describes the information in the event. It is more specific than `event.category`.\nExamples are `group-add`, `process-started`, `file-created`. The value is\nnormally defined by the implementer.', + columnHeaderType: 'not-filtered', + id: 'event.action', + category: 'event', + type: 'string', + searchable: null, + example: 'user-password-change', + }, + { + indexes: null, + aggregatable: true, + name: null, + description: + 'Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.', + columnHeaderType: 'not-filtered', + id: 'process.args', + category: 'process', + type: 'string', + searchable: null, + example: '["/usr/bin/ssh","-l","user","10.0.0.16"]', + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'process.pid', + searchable: null, + }, + { + indexes: null, + aggregatable: true, + name: null, + description: 'IP address of the source (IPv4 or IPv6).', + columnHeaderType: 'not-filtered', + id: 'source.ip', + category: 'source', + type: 'ip', + searchable: null, + }, + { + indexes: null, + aggregatable: true, + name: null, + description: 'Port of the source.', + columnHeaderType: 'not-filtered', + id: 'source.port', + category: 'source', + type: 'number', + searchable: null, + }, + { + indexes: null, + aggregatable: true, + name: null, + description: 'IP address of the destination (IPv4 or IPv6).', + columnHeaderType: 'not-filtered', + id: 'destination.ip', + category: 'destination', + type: 'ip', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'destination.port', + searchable: null, + }, + { + indexes: null, + aggregatable: true, + name: null, + description: 'Short name or login of the user.', + columnHeaderType: 'not-filtered', + id: 'user.name', + category: 'user', + type: 'string', + searchable: null, + example: 'albert', + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'host.name', + searchable: null, + }, + ], + dataProviders: [ + { + excluded: false, + and: [], + kqlQuery: '', + name: '30d47c8a1b179ae435058e5b23b96118125a451fe58efd77be288f00456ff77d', + queryMatch: { + displayValue: null, + field: '_id', + displayField: null, + value: '30d47c8a1b179ae435058e5b23b96118125a451fe58efd77be288f00456ff77d', + operator: ':', + }, + id: + 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-30d47c8a1b179ae435058e5b23b96118125a451fe58efd77be288f00456ff77d', + enabled: true, + }, + ], + description: '', + eventType: 'all', + filters: [], + kqlMode: 'filter', + kqlQuery: { + filterQuery: { + kuery: { + kind: 'kuery', + expression: '', + }, + serializedQuery: '', + }, + }, + title: 'Generic Network Timeline', + dateRange: { + start: 1588255858373, + end: 1588256218373, + }, + savedQueryId: null, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + created: 1588256264265, + createdBy: 'Elastic', + updated: 1588256629234, + updatedBy: 'Elastic', + eventNotes: [], + globalNotes: [], + pinnedEventIds: [], + timelineType: 'template', + status: 'immutable', + templateTimelineId: '2c7e0663-5a91-0002-aa15-26bf756d2c39', + templateTimelineVersion: 1, + }, + { + savedObjectId: null, + version: null, + columns: [ + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: '@timestamp', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'signal.rule.description', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'event.action', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'process.name', + searchable: null, + }, + { + indexes: null, + aggregatable: true, + name: null, + description: 'The working directory of the process.', + columnHeaderType: 'not-filtered', + id: 'process.working_directory', + category: 'process', + type: 'string', + searchable: null, + example: '/home/alice', + }, + { + indexes: null, + aggregatable: true, + name: null, + description: + 'Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.', + columnHeaderType: 'not-filtered', + id: 'process.args', + category: 'process', + type: 'string', + searchable: null, + example: '["/usr/bin/ssh","-l","user","10.0.0.16"]', + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'process.pid', + searchable: null, + }, + { + indexes: null, + aggregatable: true, + name: null, + description: 'Absolute path to the process executable.', + columnHeaderType: 'not-filtered', + id: 'process.parent.executable', + category: 'process', + type: 'string', + searchable: null, + example: '/usr/bin/ssh', + }, + { + indexes: null, + aggregatable: true, + name: null, + description: + 'Array of process arguments.\n\nMay be filtered to protect sensitive information.', + columnHeaderType: 'not-filtered', + id: 'process.parent.args', + category: 'process', + type: 'string', + searchable: null, + example: '["ssh","-l","user","10.0.0.16"]', + }, + { + indexes: null, + aggregatable: true, + name: null, + description: 'Process id.', + columnHeaderType: 'not-filtered', + id: 'process.parent.pid', + category: 'process', + type: 'number', + searchable: null, + example: '4242', + }, + { + indexes: null, + aggregatable: true, + name: null, + description: 'Short name or login of the user.', + columnHeaderType: 'not-filtered', + id: 'user.name', + category: 'user', + type: 'string', + searchable: null, + example: 'albert', + }, + { + indexes: null, + aggregatable: true, + name: null, + description: + 'Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.', + columnHeaderType: 'not-filtered', + id: 'host.name', + category: 'host', + type: 'string', + searchable: null, + }, + ], + dataProviders: [ + { + excluded: false, + and: [], + kqlQuery: '', + name: '590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde', + queryMatch: { + displayValue: null, + field: '_id', + displayField: null, + value: '590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde', + operator: ':', + }, + id: + 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde', + enabled: true, + }, + ], + description: '', + eventType: 'all', + filters: [], + kqlMode: 'filter', + kqlQuery: { + filterQuery: { + kuery: { + kind: 'kuery', + expression: '', + }, + serializedQuery: '', + }, + }, + title: 'Generic Process Timeline', + dateRange: { + start: 1588161020848, + end: 1588162280848, + }, + savedQueryId: null, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + created: 1588162404153, + createdBy: 'Elastic', + updated: 1588604767818, + updatedBy: 'Elastic', + eventNotes: [], + globalNotes: [], + pinnedEventIds: [], + timelineType: 'template', + status: 'immutable', + templateTimelineId: '2c7e0663-5a91-0003-aa15-26bf756d2c39', + templateTimelineVersion: 1, + }, + ], + timelinesToUpdate: [], + prepackagedTimelines: [], +}; + +export const mockCheckTimelinesStatusAfterInstallResult = { + timelinesToInstall: [], + timelinesToUpdate: [], + prepackagedTimelines: [ + { + savedObjectId: '4dc6b080-c4f5-11ea-90f7-5913f6a19d5c', + version: 'WzQxNywxXQ==', + columns: [ + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: '@timestamp', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'signal.rule.description', + searchable: null, + }, + { + indexes: null, + aggregatable: true, + name: null, + description: + 'The action captured by the event.\n\nThis describes the information in the event. It is more specific than `event.category`.\nExamples are `group-add`, `process-started`, `file-created`. The value is\nnormally defined by the implementer.', + columnHeaderType: 'not-filtered', + id: 'event.action', + category: 'event', + type: 'string', + searchable: null, + example: 'user-password-change', + }, + { + indexes: null, + aggregatable: true, + name: null, + description: + 'Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.', + columnHeaderType: 'not-filtered', + id: 'process.args', + category: 'process', + type: 'string', + searchable: null, + example: '["/usr/bin/ssh","-l","user","10.0.0.16"]', + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'process.pid', + searchable: null, + }, + { + indexes: null, + aggregatable: true, + name: null, + description: 'IP address of the source (IPv4 or IPv6).', + columnHeaderType: 'not-filtered', + id: 'source.ip', + category: 'source', + type: 'ip', + searchable: null, + }, + { + indexes: null, + aggregatable: true, + name: null, + description: 'Port of the source.', + columnHeaderType: 'not-filtered', + id: 'source.port', + category: 'source', + type: 'number', + searchable: null, + }, + { + indexes: null, + aggregatable: true, + name: null, + description: 'IP address of the destination (IPv4 or IPv6).', + columnHeaderType: 'not-filtered', + id: 'destination.ip', + category: 'destination', + type: 'ip', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'destination.port', + searchable: null, + }, + { + indexes: null, + aggregatable: true, + name: null, + description: 'Short name or login of the user.', + columnHeaderType: 'not-filtered', + id: 'user.name', + category: 'user', + type: 'string', + searchable: null, + example: 'albert', + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'host.name', + searchable: null, + }, + ], + dataProviders: [ + { + excluded: false, + and: [], + kqlQuery: '', + name: '30d47c8a1b179ae435058e5b23b96118125a451fe58efd77be288f00456ff77d', + queryMatch: { + displayValue: null, + field: '_id', + displayField: null, + value: '30d47c8a1b179ae435058e5b23b96118125a451fe58efd77be288f00456ff77d', + operator: ':', + }, + id: + 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-30d47c8a1b179ae435058e5b23b96118125a451fe58efd77be288f00456ff77d', + enabled: true, + }, + ], + description: '', + eventType: 'all', + filters: [], + kqlMode: 'filter', + kqlQuery: { + filterQuery: { + kuery: { + kind: 'kuery', + expression: '', + }, + serializedQuery: '', + }, + }, + title: 'Generic Network Timeline', + dateRange: { + start: 1588255858373, + end: 1588256218373, + }, + savedQueryId: null, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + timelineType: 'template', + status: 'immutable', + templateTimelineId: '2c7e0663-5a91-0002-aa15-26bf756d2c39', + templateTimelineVersion: 1, + created: 1594636843808, + createdBy: 'Elastic', + updated: 1594636843808, + updatedBy: 'Elastic', + excludedRowRendererIds: [], + favorite: [], + eventIdToNoteIds: [], + noteIds: [], + notes: [], + pinnedEventIds: [], + pinnedEventsSaveObject: [], + }, + { + savedObjectId: '4dc79ae0-c4f5-11ea-90f7-5913f6a19d5c', + version: 'WzQxOCwxXQ==', + columns: [ + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: '@timestamp', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'signal.rule.description', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'event.action', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'process.name', + searchable: null, + }, + { + indexes: null, + aggregatable: true, + name: null, + description: 'The working directory of the process.', + columnHeaderType: 'not-filtered', + id: 'process.working_directory', + category: 'process', + type: 'string', + searchable: null, + example: '/home/alice', + }, + { + indexes: null, + aggregatable: true, + name: null, + description: + 'Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.', + columnHeaderType: 'not-filtered', + id: 'process.args', + category: 'process', + type: 'string', + searchable: null, + example: '["/usr/bin/ssh","-l","user","10.0.0.16"]', + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'process.pid', + searchable: null, + }, + { + indexes: null, + aggregatable: true, + name: null, + description: 'Absolute path to the process executable.', + columnHeaderType: 'not-filtered', + id: 'process.parent.executable', + category: 'process', + type: 'string', + searchable: null, + example: '/usr/bin/ssh', + }, + { + indexes: null, + aggregatable: true, + name: null, + description: + 'Array of process arguments.\n\nMay be filtered to protect sensitive information.', + columnHeaderType: 'not-filtered', + id: 'process.parent.args', + category: 'process', + type: 'string', + searchable: null, + example: '["ssh","-l","user","10.0.0.16"]', + }, + { + indexes: null, + aggregatable: true, + name: null, + description: 'Process id.', + columnHeaderType: 'not-filtered', + id: 'process.parent.pid', + category: 'process', + type: 'number', + searchable: null, + example: '4242', + }, + { + indexes: null, + aggregatable: true, + name: null, + description: 'Short name or login of the user.', + columnHeaderType: 'not-filtered', + id: 'user.name', + category: 'user', + type: 'string', + searchable: null, + example: 'albert', + }, + { + indexes: null, + aggregatable: true, + name: null, + description: + 'Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.', + columnHeaderType: 'not-filtered', + id: 'host.name', + category: 'host', + type: 'string', + searchable: null, + }, + ], + dataProviders: [ + { + excluded: false, + and: [], + kqlQuery: '', + name: '590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde', + queryMatch: { + displayValue: null, + field: '_id', + displayField: null, + value: '590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde', + operator: ':', + }, + id: + 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde', + enabled: true, + }, + ], + description: '', + eventType: 'all', + filters: [], + kqlMode: 'filter', + kqlQuery: { + filterQuery: { + kuery: { + kind: 'kuery', + expression: '', + }, + serializedQuery: '', + }, + }, + title: 'Generic Process Timeline', + dateRange: { + start: 1588161020848, + end: 1588162280848, + }, + savedQueryId: null, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + timelineType: 'template', + status: 'immutable', + templateTimelineId: '2c7e0663-5a91-0003-aa15-26bf756d2c39', + templateTimelineVersion: 1, + created: 1594636843813, + createdBy: 'Elastic', + updated: 1594636843813, + updatedBy: 'Elastic', + excludedRowRendererIds: [], + favorite: [], + eventIdToNoteIds: [], + noteIds: [], + notes: [], + pinnedEventIds: [], + pinnedEventsSaveObject: [], + }, + { + savedObjectId: '4dc66260-c4f5-11ea-90f7-5913f6a19d5c', + version: 'WzQxNiwxXQ==', + columns: [ + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: '@timestamp', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'signal.rule.description', + searchable: null, + }, + { + indexes: null, + aggregatable: true, + name: null, + description: + 'The action captured by the event.\n\nThis describes the information in the event. It is more specific than `event.category`.\nExamples are `group-add`, `process-started`, `file-created`. The value is\nnormally defined by the implementer.', + columnHeaderType: 'not-filtered', + id: 'event.action', + category: 'event', + type: 'string', + searchable: null, + example: 'user-password-change', + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'endgame.data.rule_name', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'rule.reference', + searchable: null, + }, + { + aggregatable: true, + description: + 'Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.', + columnHeaderType: 'not-filtered', + id: 'host.name', + category: 'host', + type: 'string', + }, + { + aggregatable: true, + description: 'Operating system name, without the version.', + columnHeaderType: 'not-filtered', + id: 'host.os.name', + category: 'host', + type: 'string', + example: 'Mac OS X', + }, + ], + dataProviders: [ + { + excluded: false, + and: [], + kqlQuery: '', + name: '3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509', + queryMatch: { + displayValue: null, + field: '_id', + displayField: null, + value: '3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509', + operator: ':', + }, + id: + 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509', + enabled: true, + }, + ], + description: '', + eventType: 'all', + filters: [], + kqlMode: 'filter', + kqlQuery: { + filterQuery: { + kuery: { + kind: 'kuery', + expression: '', + }, + serializedQuery: '', + }, + }, + title: 'Generic Endpoint Timeline', + dateRange: { + start: 1588257731065, + end: 1588258391065, + }, + savedQueryId: null, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + timelineType: 'template', + status: 'immutable', + templateTimelineId: '2c7e0663-5a91-0001-aa15-26bf756d2c39', + templateTimelineVersion: 1, + created: 1594636843807, + createdBy: 'Elastic', + updated: 1594636843807, + updatedBy: 'Elastic', + excludedRowRendererIds: [], + favorite: [], + eventIdToNoteIds: [], + noteIds: [], + notes: [], + pinnedEventIds: [], + pinnedEventsSaveObject: [], + }, + ], +}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/prepackaged_timelines.ndjson b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/prepackaged_timelines.ndjson new file mode 100644 index 0000000000000..f7113a4ac395e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/prepackaged_timelines.ndjson @@ -0,0 +1 @@ +{"savedObjectId":"mocked-timeline-id-1","version":"WzExNzEyLDFd","columns":[{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"@timestamp","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"signal.rule.description","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"The action captured by the event.\n\nThis describes the information in the event. It is more specific than `event.category`.\nExamples are `group-add`, `process-started`, `file-created`. The value is\nnormally defined by the implementer.","columnHeaderType":"not-filtered","id":"event.action","category":"event","type":"string","searchable":null,"example":"user-password-change"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"endgame.data.rule_name","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"rule.reference","searchable":null},{"aggregatable":true,"description":"Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.","columnHeaderType":"not-filtered","id":"host.name","category":"host","type":"string"},{"aggregatable":true,"description":"Operating system name, without the version.","columnHeaderType":"not-filtered","id":"host.os.name","category":"host","type":"string","example":"Mac OS X"}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509","queryMatch":{"displayValue":null,"field":"_id","displayField":null,"value":"3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509","operator":":"},"id":"send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"kind":"kuery","expression":""},"serializedQuery":""}},"title":"Generic Endpoint Timeline","dateRange":{"start":1588257731065,"end":1588258391065},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"created":1588258576517,"createdBy":"elastic","updated":1588261039030,"updatedBy":"elastic","eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"timelineType":"template"} diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts index 9afe5ad533324..a314d5fb36c6d 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts @@ -11,6 +11,7 @@ import { TIMELINE_EXPORT_URL, TIMELINE_IMPORT_URL, TIMELINE_URL, + TIMELINE_PREPACKAGED_URL, } from '../../../../../common/constants'; import { SavedTimeline, TimelineType, TimelineStatus } from '../../../../../common/types/timeline'; @@ -18,6 +19,7 @@ import { requestMock } from '../../../detection_engine/routes/__mocks__'; import { updateTimelineSchema } from '../schemas/update_timelines_schema'; import { createTimelineSchema } from '../schemas/create_timelines_schema'; +import { GetTimelineByIdSchemaQuery } from '../schemas/get_timeline_by_id_schema'; const readable = new stream.Readable(); export const getExportTimelinesRequest = () => @@ -173,6 +175,19 @@ export const cleanDraftTimelinesRequest = (timelineType: TimelineType) => }, }); +export const getTimelineByIdRequest = (query: GetTimelineByIdSchemaQuery) => + requestMock.create({ + method: 'get', + path: TIMELINE_URL, + query, + }); + +export const installPrepackedTimelinesRequest = () => + requestMock.create({ + method: 'post', + path: TIMELINE_PREPACKAGED_URL, + }); + export const mockTimelinesSavedObjects = () => ({ saved_objects: [ { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts index c66bf7b192c62..a6f0ce232fa7b 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts @@ -22,6 +22,9 @@ import { TIMELINE_EXPORT_URL } from '../../../../common/constants'; import { convertSavedObjectToSavedNote } from '../../note/saved_object'; import { convertSavedObjectToSavedPinnedEvent } from '../../pinned_event/saved_object'; import { convertSavedObjectToSavedTimeline } from '../convert_saved_object_to_savedtimeline'; +import { mockGetCurrentUser } from './__mocks__/import_timelines'; +import { SecurityPluginSetup } from '../../../../../../plugins/security/server'; + jest.mock('../convert_saved_object_to_savedtimeline', () => { return { convertSavedObjectToSavedTimeline: jest.fn(), @@ -31,22 +34,30 @@ jest.mock('../convert_saved_object_to_savedtimeline', () => { jest.mock('../../note/saved_object', () => { return { convertSavedObjectToSavedNote: jest.fn(), + getNotesByTimelineId: jest.fn().mockReturnValue([]), }; }); jest.mock('../../pinned_event/saved_object', () => { return { convertSavedObjectToSavedPinnedEvent: jest.fn(), + getAllPinnedEventsByTimelineId: jest.fn().mockReturnValue([]), }; }); describe('export timelines', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + let securitySetup: SecurityPluginSetup; beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - + securitySetup = ({ + authc: { + getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), + }, + authz: {}, + } as unknown) as SecurityPluginSetup; clients.savedObjectsClient.bulkGet.mockResolvedValue(mockTimelinesSavedObjects()); ((convertSavedObjectToSavedTimeline as unknown) as jest.Mock).mockReturnValue(mockTimelines()); @@ -54,7 +65,7 @@ describe('export timelines', () => { ((convertSavedObjectToSavedPinnedEvent as unknown) as jest.Mock).mockReturnValue( mockPinnedEvents() ); - exportTimelinesRoute(server.router, createMockConfig()); + exportTimelinesRoute(server.router, createMockConfig(), securitySetup); }); describe('status codes', () => { @@ -85,7 +96,7 @@ describe('export timelines', () => { const result = server.validate(request); expect(result.badRequest.mock.calls[0][0]).toEqual( - 'Invalid value "undefined" supplied to "ids"' + 'Invalid value "undefined" supplied to "file_name"' ); }); @@ -93,12 +104,13 @@ describe('export timelines', () => { const request = requestMock.create({ method: 'get', path: TIMELINE_EXPORT_URL, - body: { id: 'someId' }, + query: { file_name: 'test.ndjson' }, + body: { ids: 'someId' }, }); const result = server.validate(request); - expect(result.badRequest.mock.calls[1][0]).toEqual( - 'Invalid value "undefined" supplied to "file_name"' + expect(result.badRequest.mock.calls[0][0]).toEqual( + 'Invalid value "someId" supplied to "ids",Invalid value "someId" supplied to "ids",Invalid value "{"ids":"someId"}" supplied to "(Partial<{ ids: (Array | null) }> | null)"' ); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.ts index 1d16cd9261069..89e38753ac926 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.ts @@ -15,8 +15,14 @@ import { exportTimelinesRequestBodySchema, } from './schemas/export_timelines_schema'; import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; +import { buildFrameworkRequest } from './utils/common'; +import { SetupPlugins } from '../../../plugin'; -export const exportTimelinesRoute = (router: IRouter, config: ConfigType) => { +export const exportTimelinesRoute = ( + router: IRouter, + config: ConfigType, + security: SetupPlugins['security'] +) => { router.post( { path: TIMELINE_EXPORT_URL, @@ -31,7 +37,8 @@ export const exportTimelinesRoute = (router: IRouter, config: ConfigType) => { async (context, request, response) => { try { const siemResponse = buildSiemResponse(response); - const savedObjectsClient = context.core.savedObjects.client; + const frameworkRequest = await buildFrameworkRequest(context, security, request); + const exportSizeLimit = config.maxTimelineImportExportSize; if (request.body?.ids != null && request.body.ids.length > exportSizeLimit) { @@ -42,8 +49,8 @@ export const exportTimelinesRoute = (router: IRouter, config: ConfigType) => { } const responseBody = await getExportTimelineByObjectIds({ - client: savedObjectsClient, - ids: request.body.ids, + frameworkRequest, + ids: request.body?.ids, }); return response.ok({ diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.test.ts new file mode 100644 index 0000000000000..30528f8563ab8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.test.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { SecurityPluginSetup } from '../../../../../../plugins/security/server'; + +import { + serverMock, + requestContextMock, + createMockConfig, +} from '../../detection_engine/routes/__mocks__'; + +import { mockGetCurrentUser } from './__mocks__/import_timelines'; +import { getTimelineByIdRequest } from './__mocks__/request_responses'; + +import { getTimeline, getTemplateTimeline } from './utils/create_timelines'; +import { getTimelineByIdRoute } from './get_timeline_by_id_route'; + +jest.mock('./utils/create_timelines', () => ({ + getTimeline: jest.fn(), + getTemplateTimeline: jest.fn(), +})); + +describe('get timeline by id', () => { + let server: ReturnType; + let securitySetup: SecurityPluginSetup; + let { context } = requestContextMock.createTools(); + + beforeEach(() => { + jest.resetModules(); + jest.resetAllMocks(); + + server = serverMock.create(); + context = requestContextMock.createTools().context; + + securitySetup = ({ + authc: { + getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), + }, + authz: {}, + } as unknown) as SecurityPluginSetup; + + getTimelineByIdRoute(server.router, createMockConfig(), securitySetup); + }); + + test('should call getTemplateTimeline if templateTimelineId is given', async () => { + const templateTimelineId = '123'; + await server.inject( + getTimelineByIdRequest({ template_timeline_id: templateTimelineId }), + context + ); + + expect((getTemplateTimeline as jest.Mock).mock.calls[0][1]).toEqual(templateTimelineId); + }); + + test('should call getTimeline if id is given', async () => { + const id = '456'; + + await server.inject(getTimelineByIdRequest({ id }), context); + + expect((getTimeline as jest.Mock).mock.calls[0][1]).toEqual(id); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.ts new file mode 100644 index 0000000000000..c4957b9d4b9e2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from '../../../../../../../src/core/server'; + +import { TIMELINE_URL } from '../../../../common/constants'; + +import { ConfigType } from '../../..'; +import { SetupPlugins } from '../../../plugin'; +import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; + +import { buildSiemResponse, transformError } from '../../detection_engine/routes/utils'; + +import { buildFrameworkRequest } from './utils/common'; +import { getTimelineByIdSchemaQuery } from './schemas/get_timeline_by_id_schema'; +import { getTimeline, getTemplateTimeline } from './utils/create_timelines'; + +export const getTimelineByIdRoute = ( + router: IRouter, + config: ConfigType, + security: SetupPlugins['security'] +) => { + router.get( + { + path: `${TIMELINE_URL}`, + validate: { query: buildRouteValidation(getTimelineByIdSchemaQuery) }, + options: { + tags: ['access:securitySolution'], + }, + }, + async (context, request, response) => { + try { + const frameworkRequest = await buildFrameworkRequest(context, security, request); + const { template_timeline_id: templateTimelineId, id } = request.query; + let res = null; + if (templateTimelineId != null) { + res = await getTemplateTimeline(frameworkRequest, templateTimelineId); + } else if (id != null) { + res = await getTimeline(frameworkRequest, id); + } + + return response.ok({ body: res ?? {} }); + } catch (err) { + const error = transformError(err); + const siemResponse = buildSiemResponse(response); + + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts index 248bf358064c0..fe5993cb0161d 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts @@ -83,10 +83,8 @@ describe('import timelines', () => { }; }); - jest.doMock('./utils/import_timelines', () => { - const originalModule = jest.requireActual('./utils/import_timelines'); + jest.doMock('./utils/get_timelines_from_stream', () => { return { - ...originalModule, getTupleDuplicateErrorsAndUniqueTimeline: mockGetTupleDuplicateErrorsAndUniqueTimeline.mockReturnValue( [mockDuplicateIdErrors, mockUniqueParsedObjects] ), @@ -173,6 +171,8 @@ describe('import timelines', () => { expect(response.body).toEqual({ success: false, success_count: 0, + timelines_installed: 0, + timelines_updated: 0, errors: [ { id: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', @@ -295,6 +295,8 @@ describe('import timelines', () => { expect(response.body).toEqual({ success: false, success_count: 0, + timelines_installed: 0, + timelines_updated: 0, errors: [ { id: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', @@ -322,6 +324,8 @@ describe('import timelines', () => { expect(response.body).toEqual({ success: false, success_count: 0, + timelines_installed: 0, + timelines_updated: 0, errors: [ { id: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', @@ -349,6 +353,8 @@ describe('import timelines', () => { expect(response.body).toEqual({ success: false, success_count: 0, + timelines_installed: 0, + timelines_updated: 0, errors: [ { id: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', @@ -458,10 +464,8 @@ describe('import timeline templates', () => { }; }); - jest.doMock('./utils/import_timelines', () => { - const originalModule = jest.requireActual('./utils/import_timelines'); + jest.doMock('./utils/get_timelines_from_stream', () => { return { - ...originalModule, getTupleDuplicateErrorsAndUniqueTimeline: mockGetTupleDuplicateErrorsAndUniqueTimeline.mockReturnValue( [mockDuplicateIdErrors, mockUniqueParsedTemplateTimelineObjects] ), @@ -719,6 +723,8 @@ describe('import timeline templates', () => { expect(response.body).toEqual({ success: false, success_count: 0, + timelines_installed: 0, + timelines_updated: 0, errors: [ { id: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', @@ -746,6 +752,8 @@ describe('import timeline templates', () => { expect(response.body).toEqual({ success: false, success_count: 0, + timelines_installed: 0, + timelines_updated: 0, errors: [ { id: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts index 56e4e81b4214b..c93983e499fb5 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts @@ -5,48 +5,19 @@ */ import { extname } from 'path'; -import { chunk, omit } from 'lodash/fp'; -import uuid from 'uuid'; -import { createPromiseFromStreams } from '../../../../../../../src/legacy/utils'; import { IRouter } from '../../../../../../../src/core/server'; import { TIMELINE_IMPORT_URL } from '../../../../common/constants'; -import { validate } from '../../../../common/validate'; import { SetupPlugins } from '../../../plugin'; import { ConfigType } from '../../../config'; import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; -import { importRulesSchema } from '../../../../common/detection_engine/schemas/response/import_rules_schema'; -import { - buildSiemResponse, - createBulkErrorObject, - BulkError, - transformError, -} from '../../detection_engine/routes/utils'; - -import { createTimelinesStreamFromNdJson } from '../create_timelines_stream_from_ndjson'; +import { buildSiemResponse, transformError } from '../../detection_engine/routes/utils'; +import { importTimelines } from './utils/import_timelines'; import { ImportTimelinesPayloadSchemaRt } from './schemas/import_timelines_schema'; -import { - buildFrameworkRequest, - CompareTimelinesStatus, - TimelineStatusActions, -} from './utils/common'; -import { - getTupleDuplicateErrorsAndUniqueTimeline, - isBulkError, - isImportRegular, - ImportTimelineResponse, - ImportTimelinesSchema, - PromiseFromStreams, - timelineSavedObjectOmittedFields, -} from './utils/import_timelines'; -import { createTimelines } from './utils/create_timelines'; -import { TimelineStatus } from '../../../../common/types/timeline'; - -const CHUNK_PARSED_OBJECT_SIZE = 10; -const DEFAULT_IMPORT_ERROR = `Something has gone wrong. We didn't handle something properly. To help us fix this, please upload your file to https://discuss.elastic.co/c/security/siem.`; +import { buildFrameworkRequest } from './utils/common'; export const importTimelinesRoute = ( router: IRouter, @@ -75,9 +46,8 @@ export const importTimelinesRoute = ( return siemResponse.error({ statusCode: 404 }); } - const { file } = request.body; + const { file, isImmutable } = request.body; const { filename } = file.hapi; - const fileExtension = extname(filename).toLowerCase(); if (fileExtension !== '.ndjson') { @@ -87,193 +57,16 @@ export const importTimelinesRoute = ( }); } - const objectLimit = config.maxTimelineImportExportSize; + const frameworkRequest = await buildFrameworkRequest(context, security, request); - const readStream = createTimelinesStreamFromNdJson(objectLimit); - const parsedObjects = await createPromiseFromStreams([ + const res = await importTimelines( file, - ...readStream, - ]); - const [duplicateIdErrors, uniqueParsedObjects] = getTupleDuplicateErrorsAndUniqueTimeline( - parsedObjects, - false + config.maxTimelineImportExportSize, + frameworkRequest, + isImmutable === 'true' ); - const chunkParseObjects = chunk(CHUNK_PARSED_OBJECT_SIZE, uniqueParsedObjects); - let importTimelineResponse: ImportTimelineResponse[] = []; - - const frameworkRequest = await buildFrameworkRequest(context, security, request); - - while (chunkParseObjects.length) { - const batchParseObjects = chunkParseObjects.shift() ?? []; - const newImportTimelineResponse = await Promise.all( - batchParseObjects.reduce>>( - (accum, parsedTimeline) => { - const importsWorkerPromise = new Promise( - async (resolve, reject) => { - if (parsedTimeline instanceof Error) { - // If the JSON object had a validation or parse error then we return - // early with the error and an (unknown) for the ruleId - resolve( - createBulkErrorObject({ - statusCode: 400, - message: parsedTimeline.message, - }) - ); - - return null; - } - - const { - savedObjectId, - pinnedEventIds, - globalNotes, - eventNotes, - status, - templateTimelineId, - templateTimelineVersion, - title, - timelineType, - version, - } = parsedTimeline; - const parsedTimelineObject = omit( - timelineSavedObjectOmittedFields, - parsedTimeline - ); - let newTimeline = null; - try { - const compareTimelinesStatus = new CompareTimelinesStatus({ - status, - timelineType, - title, - timelineInput: { - id: savedObjectId, - version, - }, - templateTimelineInput: { - id: templateTimelineId, - version: templateTimelineVersion, - }, - frameworkRequest, - }); - await compareTimelinesStatus.init(); - const isTemplateTimeline = compareTimelinesStatus.isHandlingTemplateTimeline; - if (compareTimelinesStatus.isCreatableViaImport) { - // create timeline / timeline template - newTimeline = await createTimelines({ - frameworkRequest, - timeline: { - ...parsedTimelineObject, - status: - status === TimelineStatus.draft - ? TimelineStatus.active - : status ?? TimelineStatus.active, - templateTimelineVersion: isTemplateTimeline - ? templateTimelineVersion - : null, - templateTimelineId: isTemplateTimeline - ? templateTimelineId ?? uuid.v4() - : null, - }, - pinnedEventIds: isTemplateTimeline ? null : pinnedEventIds, - notes: isTemplateTimeline ? globalNotes : [...globalNotes, ...eventNotes], - }); - - resolve({ - timeline_id: newTimeline.timeline.savedObjectId, - status_code: 200, - }); - } - - if (!compareTimelinesStatus.isHandlingTemplateTimeline) { - const errorMessage = compareTimelinesStatus.checkIsFailureCases( - TimelineStatusActions.createViaImport - ); - const message = errorMessage?.body ?? DEFAULT_IMPORT_ERROR; - - resolve( - createBulkErrorObject({ - id: savedObjectId ?? 'unknown', - statusCode: 409, - message, - }) - ); - } else { - if (compareTimelinesStatus.isUpdatableViaImport) { - // update timeline template - newTimeline = await createTimelines({ - frameworkRequest, - timeline: parsedTimelineObject, - timelineSavedObjectId: compareTimelinesStatus.timelineId, - timelineVersion: compareTimelinesStatus.timelineVersion, - notes: globalNotes, - existingNoteIds: compareTimelinesStatus.timelineInput.data?.noteIds, - }); - - resolve({ - timeline_id: newTimeline.timeline.savedObjectId, - status_code: 200, - }); - } else { - const errorMessage = compareTimelinesStatus.checkIsFailureCases( - TimelineStatusActions.updateViaImport - ); - - const message = errorMessage?.body ?? DEFAULT_IMPORT_ERROR; - - resolve( - createBulkErrorObject({ - id: savedObjectId ?? 'unknown', - statusCode: 409, - message, - }) - ); - } - } - } catch (err) { - resolve( - createBulkErrorObject({ - id: savedObjectId ?? 'unknown', - statusCode: 400, - message: err.message, - }) - ); - } - } - ); - return [...accum, importsWorkerPromise]; - }, - [] - ) - ); - importTimelineResponse = [ - ...duplicateIdErrors, - ...importTimelineResponse, - ...newImportTimelineResponse, - ]; - } - - const errorsResp = importTimelineResponse.filter((resp) => { - return isBulkError(resp); - }) as BulkError[]; - const successes = importTimelineResponse.filter((resp) => { - if (isImportRegular(resp)) { - return resp.status_code === 200; - } else { - return false; - } - }); - const importTimelines: ImportTimelinesSchema = { - success: errorsResp.length === 0, - success_count: successes.length, - errors: errorsResp, - }; - const [validated, errors] = validate(importTimelines, importRulesSchema); - - if (errors != null) { - return siemResponse.error({ statusCode: 500, body: errors }); - } else { - return response.ok({ body: validated ?? {} }); - } + if (typeof res !== 'string') return response.ok({ body: res ?? {} }); + else throw res; } catch (err) { const error = transformError(err); const siemResponse = buildSiemResponse(response); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/install_prepacked_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/install_prepacked_timelines_route.test.ts new file mode 100644 index 0000000000000..1fd2d40b02819 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/install_prepacked_timelines_route.test.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import { SecurityPluginSetup } from '../../../../../../plugins/security/server'; + +import { + serverMock, + requestContextMock, + createMockConfig, +} from '../../detection_engine/routes/__mocks__'; + +import { + mockGetCurrentUser, + mockCheckTimelinesStatusBeforeInstallResult, + mockCheckTimelinesStatusAfterInstallResult, +} from './__mocks__/import_timelines'; +import { installPrepackedTimelinesRequest } from './__mocks__/request_responses'; + +import { installPrepackagedTimelines } from './utils/install_prepacked_timelines'; +import { checkTimelinesStatus } from './utils/check_timelines_status'; + +import { installPrepackedTimelinesRoute } from './install_prepacked_timelines_route'; + +jest.mock('./utils/install_prepacked_timelines', () => ({ + installPrepackagedTimelines: jest.fn(), +})); + +jest.mock('./utils/check_timelines_status', () => ({ + checkTimelinesStatus: jest.fn(), +})); + +describe('installPrepackagedTimelines', () => { + let server: ReturnType; + let securitySetup: SecurityPluginSetup; + let { context } = requestContextMock.createTools(); + + beforeEach(() => { + jest.resetModules(); + jest.resetAllMocks(); + + server = serverMock.create(); + context = requestContextMock.createTools().context; + + securitySetup = ({ + authc: { + getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), + }, + authz: {}, + } as unknown) as SecurityPluginSetup; + + installPrepackedTimelinesRoute(server.router, createMockConfig(), securitySetup); + }); + + test('should call installPrepackagedTimelines ', async () => { + (checkTimelinesStatus as jest.Mock).mockReturnValue( + mockCheckTimelinesStatusBeforeInstallResult + ); + + await server.inject(installPrepackedTimelinesRequest(), context); + + expect(installPrepackagedTimelines).toHaveBeenCalled(); + }); + + test('should return installPrepackagedTimelines result ', async () => { + (checkTimelinesStatus as jest.Mock).mockReturnValue( + mockCheckTimelinesStatusBeforeInstallResult + ); + (installPrepackagedTimelines as jest.Mock).mockReturnValue({ + errors: [], + success: true, + success_count: 3, + timelines_installed: 3, + timelines_updated: 0, + }); + + const result = await server.inject(installPrepackedTimelinesRequest(), context); + + expect(result.body).toEqual({ + errors: [], + success: true, + success_count: 3, + timelines_installed: 3, + timelines_updated: 0, + }); + }); + + test('should not call installPrepackagedTimelines if it has nothing to install or update', async () => { + (checkTimelinesStatus as jest.Mock).mockReturnValue(mockCheckTimelinesStatusAfterInstallResult); + + await server.inject(installPrepackedTimelinesRequest(), context); + + expect(installPrepackagedTimelines).not.toHaveBeenCalled(); + }); + + test('should return success if it has nothing to install or update', async () => { + (checkTimelinesStatus as jest.Mock).mockReturnValue(mockCheckTimelinesStatusAfterInstallResult); + + const result = await server.inject(installPrepackedTimelinesRequest(), context); + + expect(result.body).toEqual({ + errors: [], + success: true, + success_count: 0, + timelines_installed: 0, + timelines_updated: 0, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/install_prepacked_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/install_prepacked_timelines_route.ts new file mode 100644 index 0000000000000..aba05054abfe2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/install_prepacked_timelines_route.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from '../../../../../../../src/core/server'; + +import { TIMELINE_PREPACKAGED_URL } from '../../../../common/constants'; + +import { SetupPlugins } from '../../../plugin'; +import { ConfigType } from '../../../config'; +import { validate } from '../../../../common/validate'; + +import { buildSiemResponse, transformError } from '../../detection_engine/routes/utils'; + +import { installPrepackagedTimelines } from './utils/install_prepacked_timelines'; + +import { checkTimelinesStatus } from './utils/check_timelines_status'; + +import { checkTimelineStatusRt } from './schemas/check_timelines_status_schema'; +import { buildFrameworkRequest } from './utils/common'; + +export const installPrepackedTimelinesRoute = ( + router: IRouter, + config: ConfigType, + security: SetupPlugins['security'] +) => { + router.post( + { + path: `${TIMELINE_PREPACKAGED_URL}`, + validate: {}, + options: { + tags: ['access:securitySolution'], + body: { + maxBytes: config.maxTimelineImportPayloadBytes, + output: 'stream', + }, + }, + }, + async (context, request, response) => { + try { + const frameworkRequest = await buildFrameworkRequest(context, security, request); + const prepackagedTimelineStatus = await checkTimelinesStatus(frameworkRequest); + const [validatedprepackagedTimelineStatus, prepackagedTimelineStatusError] = validate( + prepackagedTimelineStatus, + checkTimelineStatusRt + ); + + if (prepackagedTimelineStatusError != null) { + throw prepackagedTimelineStatusError; + } + + const timelinesToInstalled = + validatedprepackagedTimelineStatus?.timelinesToInstall.length ?? 0; + const timelinesNotUpdated = + validatedprepackagedTimelineStatus?.timelinesToUpdate.length ?? 0; + let res = null; + + if (timelinesToInstalled > 0 || timelinesNotUpdated > 0) { + res = await installPrepackagedTimelines( + config.maxTimelineImportExportSize, + frameworkRequest, + true + ); + } + if (res instanceof Error) { + throw res; + } else { + return response.ok({ + body: res ?? { + success: true, + success_count: 0, + timelines_installed: 0, + timelines_updated: 0, + errors: [], + }, + }); + } + } catch (err) { + const error = transformError(err); + const siemResponse = buildSiemResponse(response); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/check_timelines_status_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/check_timelines_status_schema.ts new file mode 100644 index 0000000000000..f21ce5689a03b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/check_timelines_status_schema.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as rt from 'io-ts'; +import { TimelineSavedToReturnObjectRuntimeType } from '../../../../../common/types/timeline'; + +import { ImportTimelinesSchemaRt } from './import_timelines_schema'; +import { unionWithNullType } from '../../../../../common/utility_types'; + +export const checkTimelineStatusRt = rt.type({ + timelinesToInstall: rt.array(unionWithNullType(ImportTimelinesSchemaRt)), + timelinesToUpdate: rt.array(unionWithNullType(ImportTimelinesSchemaRt)), + prepackagedTimelines: rt.array(unionWithNullType(TimelineSavedToReturnObjectRuntimeType)), +}); + +export type CheckTimelineStatusRt = rt.TypeOf; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/export_timelines_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/export_timelines_schema.ts index 9264f1e3e5047..ce8eb93bdbdbd 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/export_timelines_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/export_timelines_schema.ts @@ -5,11 +5,14 @@ */ import * as rt from 'io-ts'; +import { unionWithNullType } from '../../../../../common/utility_types'; export const exportTimelinesQuerySchema = rt.type({ file_name: rt.string, }); -export const exportTimelinesRequestBodySchema = rt.type({ - ids: rt.array(rt.string), -}); +export const exportTimelinesRequestBodySchema = unionWithNullType( + rt.partial({ + ids: unionWithNullType(rt.array(rt.string)), + }) +); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts new file mode 100644 index 0000000000000..2c6098bc75500 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import * as rt from 'io-ts'; + +export const getTimelineByIdSchemaQuery = rt.partial({ + template_timeline_id: rt.string, + id: rt.string, +}); + +export type GetTimelineByIdSchemaQuery = rt.TypeOf; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/import_timelines_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/import_timelines_schema.ts index 9045f2b3f35d2..afce9d6cdcb24 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/import_timelines_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/import_timelines_schema.ts @@ -26,6 +26,8 @@ export const ImportTimelinesSchemaRt = rt.intersection([ }), ]); +export type ImportTimelinesSchema = rt.TypeOf; + const ReadableRt = new rt.Type( 'ReadableRt', (u): u is Readable => u instanceof Readable, @@ -36,11 +38,17 @@ const ReadableRt = new rt.Type( }), (a) => a ); -export const ImportTimelinesPayloadSchemaRt = rt.type({ - file: rt.intersection([ - ReadableRt, - rt.type({ - hapi: rt.type({ filename: rt.string }), - }), - ]), -}); + +const booleanInString = rt.union([rt.literal('true'), rt.literal('false')]); + +export const ImportTimelinesPayloadSchemaRt = rt.intersection([ + rt.type({ + file: rt.intersection([ + ReadableRt, + rt.type({ + hapi: rt.type({ filename: rt.string }), + }), + ]), + }), + rt.partial({ isImmutable: booleanInString }), +]); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/check_timelines_status.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/check_timelines_status.ts new file mode 100644 index 0000000000000..2ce2c37d4fa31 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/check_timelines_status.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import path, { join, resolve } from 'path'; + +import { TimelineSavedObject } from '../../../../../common/types/timeline'; + +import { FrameworkRequest } from '../../../framework'; + +import { getExistingPrepackagedTimelines } from '../../saved_object'; + +import { CheckTimelineStatusRt } from '../schemas/check_timelines_status_schema'; + +import { loadData, getReadables } from './common'; +import { getTimelinesToInstall } from './get_timelines_to_install'; +import { getTimelinesToUpdate } from './get_timelines_to_update'; + +export const checkTimelinesStatus = async ( + frameworkRequest: FrameworkRequest, + filePath?: string, + fileName?: string +): Promise => { + let readStream; + let timeline: { + totalCount: number; + timeline: TimelineSavedObject[]; + }; + const dir = resolve( + join(__dirname, filePath ?? '../../../detection_engine/rules/prepackaged_timelines') + ); + const file = fileName ?? 'index.ndjson'; + const dataPath = path.join(dir, file); + + try { + readStream = await getReadables(dataPath); + timeline = await getExistingPrepackagedTimelines(frameworkRequest, false); + } catch (err) { + return { + timelinesToInstall: [], + timelinesToUpdate: [], + prepackagedTimelines: [], + }; + } + + return loadData<'utf-8', CheckTimelineStatusRt>( + readStream, + (timelinesFromFileSystem: T) => { + if (Array.isArray(timelinesFromFileSystem)) { + const parsedTimelinesFromFileSystem = timelinesFromFileSystem.map((t: string) => + JSON.parse(t) + ); + const prepackagedTimelines = timeline.timeline ?? []; + const timelinesToInstall = getTimelinesToInstall( + parsedTimelinesFromFileSystem, + prepackagedTimelines + ); + const timelinesToUpdate = getTimelinesToUpdate( + parsedTimelinesFromFileSystem, + prepackagedTimelines + ); + + return Promise.resolve({ + timelinesToInstall, + timelinesToUpdate, + prepackagedTimelines, + }); + } else { + return Promise.reject(new Error('load timeline error')); + } + }, + 'utf-8' + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts index 2c2d651fd483b..6eefdb0bfc5ec 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts @@ -4,9 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ import { set } from 'lodash/fp'; +import readline from 'readline'; +import fs from 'fs'; +import { Readable } from 'stream'; import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; +import { createListStream } from '../../../../../../../../src/legacy/utils'; + import { SetupPlugins } from '../../../../plugin'; import { FrameworkRequest } from '../../../framework'; @@ -30,6 +35,103 @@ export const buildFrameworkRequest = async ( ); }; +export const getReadables = (dataPath: string): Promise => + new Promise((resolved, reject) => { + const contents: string[] = []; + const readable = fs.createReadStream(dataPath, { encoding: 'utf-8' }); + + readable.on('data', (stream) => { + contents.push(stream); + }); + + readable.on('end', () => { + const streams = createListStream(contents); + resolved(streams); + }); + + readable.on('error', (err) => { + reject(err); + }); + }); + +export const loadData = ( + readStream: Readable, + bulkInsert: (docs: V) => Promise, + encoding?: T, + maxTimelineImportExportSize?: number | null +): Promise => { + return new Promise((resolved, reject) => { + let docs: string[] = []; + let isPaused: boolean = false; + + const lineStream = readline.createInterface({ input: readStream }); + const onClose = async () => { + if (docs.length > 0) { + try { + let bulkInsertResult; + if (typeof encoding === 'string' && encoding === 'utf-8') { + bulkInsertResult = await bulkInsert(docs); + } else { + const docstmp = createListStream(docs.join('\n')); + bulkInsertResult = await bulkInsert(docstmp); + } + resolved(bulkInsertResult); + } catch (err) { + reject(err); + return; + } + } + reject(new Error('No data provided')); + }; + + const closeWithError = (err: Error) => { + lineStream.removeListener('close', onClose); + lineStream.close(); + reject(err); + }; + + lineStream.on('close', onClose); + + lineStream.on('line', async (line) => { + if (line.length === 0 || line.charAt(0) === '/' || line.charAt(0) === ' ') { + return; + } + + docs.push(line); + + if ( + maxTimelineImportExportSize != null && + docs.length >= maxTimelineImportExportSize && + !isPaused + ) { + lineStream.pause(); + + const docstmp = createListStream(docs.join('\n')); + docs = []; + + try { + if (typeof encoding === 'string' && encoding === 'utf-8') { + await bulkInsert(docs); + } else { + await bulkInsert(docstmp); + } + lineStream.resume(); + } catch (err) { + closeWithError(err); + } + } + }); + + lineStream.on('pause', async () => { + isPaused = true; + }); + + lineStream.on('resume', async () => { + isPaused = false; + }); + }); +}; + export enum TimelineStatusActions { create = 'create', createViaImport = 'createViaImport', diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts index 67965469e1a9f..cdedffbbd9458 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts @@ -16,10 +16,18 @@ import { NoteResult, ResponseTimeline } from '../../../../graphql/types'; export const saveTimelines = ( frameworkRequest: FrameworkRequest, timeline: SavedTimeline, - timelineSavedObjectId: string | null = null, - timelineVersion: string | null = null -): Promise => - timelineLib.persistTimeline(frameworkRequest, timelineSavedObjectId, timelineVersion, timeline); + timelineSavedObjectId?: string | null, + timelineVersion?: string | null, + isImmutable?: boolean +): Promise => { + return timelineLib.persistTimeline( + frameworkRequest, + timelineSavedObjectId ?? null, + timelineVersion ?? null, + timeline, + isImmutable + ); +}; export const savePinnedEvents = ( frameworkRequest: FrameworkRequest, @@ -70,6 +78,7 @@ interface CreateTimelineProps { pinnedEventIds?: string[] | null; notes?: NoteResult[]; existingNoteIds?: string[]; + isImmutable?: boolean; } export const createTimelines = async ({ @@ -80,12 +89,14 @@ export const createTimelines = async ({ pinnedEventIds = null, notes = [], existingNoteIds = [], + isImmutable, }: CreateTimelineProps): Promise => { const responseTimeline = await saveTimelines( frameworkRequest, timeline, timelineSavedObjectId, - timelineVersion + timelineVersion, + isImmutable ); const newTimelineSavedObjectId = responseTimeline.timeline.savedObjectId; const newTimelineVersion = responseTimeline.timeline.version; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts index f4b97ac3510cc..6f194c3b8538e 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts @@ -6,17 +6,9 @@ import { omit } from 'lodash/fp'; -import { - SavedObjectsClient, - SavedObjectsFindOptions, - SavedObjectsFindResponse, -} from '../../../../../../../../src/core/server'; - import { ExportedTimelines, - ExportTimelineSavedObjectsClient, ExportedNotes, - TimelineSavedObject, ExportTimelineNotFoundError, } from '../../../../../common/types/timeline'; import { NoteSavedObject } from '../../../../../common/types/timeline/note'; @@ -24,71 +16,11 @@ import { PinnedEventSavedObject } from '../../../../../common/types/timeline/pin import { transformDataToNdjson } from '../../../../utils/read_stream/create_stream_from_ndjson'; -import { convertSavedObjectToSavedPinnedEvent } from '../../../pinned_event/saved_object'; -import { convertSavedObjectToSavedNote } from '../../../note/saved_object'; -import { pinnedEventSavedObjectType } from '../../../pinned_event/saved_object_mappings'; -import { noteSavedObjectType } from '../../../note/saved_object_mappings'; - -import { timelineSavedObjectType } from '../../saved_object_mappings'; -import { convertSavedObjectToSavedTimeline } from '../../convert_saved_object_to_savedtimeline'; - -export type TimelineSavedObjectsClient = Pick< - SavedObjectsClient, - | 'get' - | 'errors' - | 'create' - | 'bulkCreate' - | 'delete' - | 'find' - | 'bulkGet' - | 'update' - | 'bulkUpdate' ->; - -const getAllSavedPinnedEvents = ( - pinnedEventsSavedObjects: SavedObjectsFindResponse -): PinnedEventSavedObject[] => { - return pinnedEventsSavedObjects != null - ? (pinnedEventsSavedObjects?.saved_objects ?? []).map((savedObject) => - convertSavedObjectToSavedPinnedEvent(savedObject) - ) - : []; -}; - -const getPinnedEventsByTimelineId = ( - savedObjectsClient: ExportTimelineSavedObjectsClient, - timelineId: string -): Promise> => { - const options: SavedObjectsFindOptions = { - type: pinnedEventSavedObjectType, - search: timelineId, - searchFields: ['timelineId'], - }; - return savedObjectsClient.find(options); -}; +import { FrameworkRequest } from '../../../framework'; +import * as noteLib from '../../../note/saved_object'; +import * as pinnedEventLib from '../../../pinned_event/saved_object'; -const getAllSavedNote = ( - noteSavedObjects: SavedObjectsFindResponse -): NoteSavedObject[] => { - return noteSavedObjects != null - ? noteSavedObjects.saved_objects.map((savedObject) => - convertSavedObjectToSavedNote(savedObject) - ) - : []; -}; - -const getNotesByTimelineId = ( - savedObjectsClient: ExportTimelineSavedObjectsClient, - timelineId: string -): Promise> => { - const options: SavedObjectsFindOptions = { - type: noteSavedObjectType, - search: timelineId, - searchFields: ['timelineId'], - }; - - return savedObjectsClient.find(options); -}; +import { getTimelines } from '../../saved_object'; const getGlobalEventNotesByTimelineId = (currentNotes: NoteSavedObject[]): ExportedNotes => { const initialNotes: ExportedNotes = { @@ -119,64 +51,30 @@ const getPinnedEventsIdsByTimelineId = ( return currentPinnedEvents.map((event) => event.eventId) ?? []; }; -const getTimelines = async ( - savedObjectsClient: ExportTimelineSavedObjectsClient, - timelineIds: string[] -) => { - const savedObjects = await Promise.resolve( - savedObjectsClient.bulkGet( - timelineIds.reduce( - (acc, timelineId) => [...acc, { id: timelineId, type: timelineSavedObjectType }], - [] as Array<{ id: string; type: string }> - ) - ) - ); - - const timelineObjects: { - timelines: TimelineSavedObject[]; - errors: ExportTimelineNotFoundError[]; - } = savedObjects.saved_objects.reduce( - (acc, savedObject) => { - return savedObject.error == null - ? { - errors: acc.errors, - timelines: [...acc.timelines, convertSavedObjectToSavedTimeline(savedObject)], - } - : { errors: [...acc.errors, savedObject.error], timelines: acc.timelines }; - }, - { - timelines: [] as TimelineSavedObject[], - errors: [] as ExportTimelineNotFoundError[], - } - ); - - return timelineObjects; -}; - const getTimelinesFromObjects = async ( - savedObjectsClient: ExportTimelineSavedObjectsClient, - ids: string[] + request: FrameworkRequest, + ids?: string[] | null ): Promise> => { - const { timelines, errors } = await getTimelines(savedObjectsClient, ids); + const { timelines, errors } = await getTimelines(request, ids); + const exportedIds = timelines.map((t) => t.savedObjectId); - const [notes, pinnedEventIds] = await Promise.all([ - Promise.all(ids.map((timelineId) => getNotesByTimelineId(savedObjectsClient, timelineId))), + const [notes, pinnedEvents] = await Promise.all([ + Promise.all(exportedIds.map((timelineId) => noteLib.getNotesByTimelineId(request, timelineId))), Promise.all( - ids.map((timelineId) => getPinnedEventsByTimelineId(savedObjectsClient, timelineId)) + exportedIds.map((timelineId) => + pinnedEventLib.getAllPinnedEventsByTimelineId(request, timelineId) + ) ), ]); - const myNotes = notes.reduce( - (acc, note) => [...acc, ...getAllSavedNote(note)], - [] - ); + const myNotes = notes.reduce((acc, note) => [...acc, ...note], []); - const myPinnedEventIds = pinnedEventIds.reduce( - (acc, pinnedEventId) => [...acc, ...getAllSavedPinnedEvents(pinnedEventId)], + const myPinnedEventIds = pinnedEvents.reduce( + (acc, pinnedEventId) => [...acc, ...pinnedEventId], [] ); - const myResponse = ids.reduce((acc, timelineId) => { + const myResponse = exportedIds.reduce((acc, timelineId) => { const myTimeline = timelines.find((t) => t.savedObjectId === timelineId); if (myTimeline != null) { const timelineNotes = myNotes.filter((n) => n.timelineId === timelineId); @@ -198,12 +96,12 @@ const getTimelinesFromObjects = async ( }; export const getExportTimelineByObjectIds = async ({ - client, + frameworkRequest, ids, }: { - client: ExportTimelineSavedObjectsClient; - ids: string[]; + frameworkRequest: FrameworkRequest; + ids?: string[] | null; }) => { - const timeline = await getTimelinesFromObjects(client, ids); + const timeline = await getTimelinesFromObjects(frameworkRequest, ids); return transformDataToNdjson(timeline); }; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines.ts new file mode 100644 index 0000000000000..1dac773ad6fde --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FrameworkRequest } from '../../../framework'; +import { getTimelines as getSelectedTimelines } from '../../saved_object'; +import { TimelineSavedObject } from '../../../../../common/types/timeline'; + +export const getTimelines = async ( + frameworkRequest: FrameworkRequest, + ids: string[] +): Promise<{ timeline: TimelineSavedObject[] | null; error: string | null }> => { + try { + const timelines = await getSelectedTimelines(frameworkRequest, ids); + const existingTimelineIds = timelines.timelines.map((timeline) => timeline.savedObjectId); + const errorMsg = timelines.errors.reduce( + (acc, curr) => (acc ? `${acc}, ${curr.message}` : curr.message), + '' + ); + if (existingTimelineIds.length > 0) { + const message = existingTimelineIds.join(', '); + return { + timeline: timelines.timelines, + error: errorMsg ? `${message} found, ${errorMsg}` : null, + }; + } else { + return { timeline: null, error: errorMsg }; + } + } catch (e) { + return e.message; + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines_from_stream.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines_from_stream.ts new file mode 100644 index 0000000000000..85e35d055767e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines_from_stream.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import uuid from 'uuid'; +import { createBulkErrorObject, BulkError } from '../../../detection_engine/routes/utils'; +import { PromiseFromStreams } from './import_timelines'; + +export const getTupleDuplicateErrorsAndUniqueTimeline = ( + timelines: PromiseFromStreams[], + isOverwrite: boolean +): [BulkError[], PromiseFromStreams[]] => { + const { errors, timelinesAcc } = timelines.reduce( + (acc, parsedTimeline) => { + if (parsedTimeline instanceof Error) { + acc.timelinesAcc.set(uuid.v4(), parsedTimeline); + } else { + const { savedObjectId } = parsedTimeline; + if (savedObjectId != null) { + if (acc.timelinesAcc.has(savedObjectId) && !isOverwrite) { + acc.errors.set( + uuid.v4(), + createBulkErrorObject({ + id: savedObjectId, + statusCode: 400, + message: `More than one timeline with savedObjectId: "${savedObjectId}" found`, + }) + ); + } + acc.timelinesAcc.set(savedObjectId, parsedTimeline); + } else { + acc.timelinesAcc.set(uuid.v4(), parsedTimeline); + } + } + + return acc; + }, // using map (preserves ordering) + { + errors: new Map(), + timelinesAcc: new Map(), + } + ); + + return [Array.from(errors.values()), Array.from(timelinesAcc.values())]; +}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines_to_install.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines_to_install.ts new file mode 100644 index 0000000000000..096ff48a82176 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines_to_install.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ImportTimelinesSchema } from '../schemas/import_timelines_schema'; +import { TimelineSavedObject } from '../../../../../common/types/timeline'; + +export const getTimelinesToInstall = ( + timelinesFromFileSystem: ImportTimelinesSchema[], + installedTimelines: TimelineSavedObject[] +): ImportTimelinesSchema[] => { + return timelinesFromFileSystem.filter( + (timeline) => + !installedTimelines.some( + (installedTimeline) => installedTimeline.templateTimelineId === timeline.templateTimelineId + ) + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines_to_update.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines_to_update.ts new file mode 100644 index 0000000000000..51ede7feee83a --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines_to_update.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ImportTimelinesSchema } from '../schemas/import_timelines_schema'; +import { TimelineSavedObject } from '../../../../../common/types/timeline'; + +export const getTimelinesToUpdate = ( + timelinesFromFileSystem: ImportTimelinesSchema[], + installedTimelines: TimelineSavedObject[] +): ImportTimelinesSchema[] => { + return timelinesFromFileSystem.filter((timeline) => + installedTimelines.some((installedTimeline) => { + return ( + timeline.templateTimelineId === installedTimeline.templateTimelineId && + timeline.templateTimelineVersion! > installedTimeline.templateTimelineVersion! + ); + }) + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts index 3f46b9ba91dc4..996dc5823691d 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts @@ -4,18 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ +import { has, chunk, omit } from 'lodash/fp'; +import { Readable } from 'stream'; import uuid from 'uuid'; -import { has } from 'lodash/fp'; -import { createBulkErrorObject, BulkError } from '../../../detection_engine/routes/utils'; -import { SavedTimeline } from '../../../../../common/types/timeline'; + +import { + TimelineStatus, + SavedTimeline, + ImportTimelineResultSchema, + importTimelineResultSchema, +} from '../../../../../common/types/timeline'; +import { validate } from '../../../../../common/validate'; import { NoteResult } from '../../../../graphql/types'; import { HapiReadableStream } from '../../../detection_engine/rules/types'; +import { createBulkErrorObject, BulkError } from '../../../detection_engine/routes/utils'; +import { createTimelines } from './create_timelines'; +import { FrameworkRequest } from '../../../framework'; +import { createTimelinesStreamFromNdJson } from '../../create_timelines_stream_from_ndjson'; +import { createPromiseFromStreams } from '../../../../../../../../src/legacy/utils'; -export interface ImportTimelinesSchema { - success: boolean; - success_count: number; - errors: BulkError[]; -} +import { getTupleDuplicateErrorsAndUniqueTimeline } from './get_timelines_from_stream'; +import { CompareTimelinesStatus } from './compare_timelines_status'; +import { TimelineStatusActions } from './common'; export type ImportedTimeline = SavedTimeline & { savedObjectId: string | null; @@ -25,56 +35,20 @@ export type ImportedTimeline = SavedTimeline & { eventNotes: NoteResult[]; }; +export type PromiseFromStreams = ImportedTimeline; + interface ImportRegular { timeline_id: string; status_code: number; message?: string; + action: TimelineStatusActions.createViaImport | TimelineStatusActions.updateViaImport; } export type ImportTimelineResponse = ImportRegular | BulkError; -export type PromiseFromStreams = ImportedTimeline; export interface ImportTimelinesRequestParams { body: { file: HapiReadableStream }; } -export const getTupleDuplicateErrorsAndUniqueTimeline = ( - timelines: PromiseFromStreams[], - isOverwrite: boolean -): [BulkError[], PromiseFromStreams[]] => { - const { errors, timelinesAcc } = timelines.reduce( - (acc, parsedTimeline) => { - if (parsedTimeline instanceof Error) { - acc.timelinesAcc.set(uuid.v4(), parsedTimeline); - } else { - const { savedObjectId } = parsedTimeline; - if (savedObjectId != null) { - if (acc.timelinesAcc.has(savedObjectId) && !isOverwrite) { - acc.errors.set( - uuid.v4(), - createBulkErrorObject({ - id: savedObjectId, - statusCode: 400, - message: `More than one timeline with savedObjectId: "${savedObjectId}" found`, - }) - ); - } - acc.timelinesAcc.set(savedObjectId, parsedTimeline); - } else { - acc.timelinesAcc.set(uuid.v4(), parsedTimeline); - } - } - - return acc; - }, // using map (preserves ordering) - { - errors: new Map(), - timelinesAcc: new Map(), - } - ); - - return [Array.from(errors.values()), Array.from(timelinesAcc.values())]; -}; - export const isImportRegular = ( importTimelineResponse: ImportTimelineResponse ): importTimelineResponse is ImportRegular => { @@ -102,3 +76,205 @@ export const timelineSavedObjectOmittedFields = [ 'updatedBy', 'version', ]; + +const CHUNK_PARSED_OBJECT_SIZE = 10; +const DEFAULT_IMPORT_ERROR = `Something has gone wrong. We didn't handle something properly. To help us fix this, please upload your file to https://discuss.elastic.co/c/security/siem.`; + +export const importTimelines = async ( + file: Readable, + maxTimelineImportExportSize: number, + frameworkRequest: FrameworkRequest, + isImmutable?: boolean +): Promise => { + const readStream = createTimelinesStreamFromNdJson(maxTimelineImportExportSize); + const parsedObjects = await createPromiseFromStreams([file, ...readStream]); + + const [duplicateIdErrors, uniqueParsedObjects] = getTupleDuplicateErrorsAndUniqueTimeline( + parsedObjects, + false + ); + + const chunkParseObjects = chunk(CHUNK_PARSED_OBJECT_SIZE, uniqueParsedObjects); + let importTimelineResponse: ImportTimelineResponse[] = []; + + while (chunkParseObjects.length) { + const batchParseObjects = chunkParseObjects.shift() ?? []; + const newImportTimelineResponse = await Promise.all( + batchParseObjects.reduce>>((accum, parsedTimeline) => { + const importsWorkerPromise = new Promise( + async (resolve, reject) => { + if (parsedTimeline instanceof Error) { + // If the JSON object had a validation or parse error then we return + // early with the error and an (unknown) for the ruleId + resolve( + createBulkErrorObject({ + statusCode: 400, + message: parsedTimeline.message, + }) + ); + + return null; + } + + const { + savedObjectId, + pinnedEventIds, + globalNotes, + eventNotes, + status, + templateTimelineId, + templateTimelineVersion, + title, + timelineType, + version, + } = parsedTimeline; + const parsedTimelineObject = omit(timelineSavedObjectOmittedFields, parsedTimeline); + let newTimeline = null; + try { + const compareTimelinesStatus = new CompareTimelinesStatus({ + status, + timelineType, + title, + timelineInput: { + id: savedObjectId, + version, + }, + templateTimelineInput: { + id: templateTimelineId, + version: templateTimelineVersion, + }, + frameworkRequest, + }); + await compareTimelinesStatus.init(); + const isTemplateTimeline = compareTimelinesStatus.isHandlingTemplateTimeline; + if (compareTimelinesStatus.isCreatableViaImport) { + // create timeline / timeline template + newTimeline = await createTimelines({ + frameworkRequest, + timeline: { + ...parsedTimelineObject, + status: + status === TimelineStatus.draft + ? TimelineStatus.active + : status ?? TimelineStatus.active, + templateTimelineVersion: isTemplateTimeline ? templateTimelineVersion : null, + templateTimelineId: isTemplateTimeline ? templateTimelineId ?? uuid.v4() : null, + }, + pinnedEventIds: isTemplateTimeline ? null : pinnedEventIds, + notes: isTemplateTimeline ? globalNotes : [...globalNotes, ...eventNotes], + isImmutable, + }); + + resolve({ + timeline_id: newTimeline.timeline.savedObjectId, + status_code: 200, + action: TimelineStatusActions.createViaImport, + }); + } + + if (!compareTimelinesStatus.isHandlingTemplateTimeline) { + const errorMessage = compareTimelinesStatus.checkIsFailureCases( + TimelineStatusActions.createViaImport + ); + const message = errorMessage?.body ?? DEFAULT_IMPORT_ERROR; + + resolve( + createBulkErrorObject({ + id: savedObjectId ?? 'unknown', + statusCode: 409, + message, + }) + ); + } else { + if (compareTimelinesStatus.isUpdatableViaImport) { + // update timeline template + newTimeline = await createTimelines({ + frameworkRequest, + timeline: parsedTimelineObject, + timelineSavedObjectId: compareTimelinesStatus.timelineId, + timelineVersion: compareTimelinesStatus.timelineVersion, + notes: globalNotes, + existingNoteIds: compareTimelinesStatus.timelineInput.data?.noteIds, + isImmutable, + }); + + resolve({ + timeline_id: newTimeline.timeline.savedObjectId, + status_code: 200, + action: TimelineStatusActions.updateViaImport, + }); + } else { + const errorMessage = compareTimelinesStatus.checkIsFailureCases( + TimelineStatusActions.updateViaImport + ); + + const message = errorMessage?.body ?? DEFAULT_IMPORT_ERROR; + + resolve( + createBulkErrorObject({ + id: + savedObjectId ?? + (templateTimelineId + ? `(template_timeline_id) ${templateTimelineId}` + : 'unknown'), + statusCode: 409, + message, + }) + ); + } + } + } catch (err) { + resolve( + createBulkErrorObject({ + id: + savedObjectId ?? + (templateTimelineId + ? `(template_timeline_id) ${templateTimelineId}` + : 'unknown'), + statusCode: 400, + message: err.message, + }) + ); + } + } + ); + return [...accum, importsWorkerPromise]; + }, []) + ); + importTimelineResponse = [ + ...duplicateIdErrors, + ...importTimelineResponse, + ...newImportTimelineResponse, + ]; + } + + const errorsResp = importTimelineResponse.filter((resp) => { + return isBulkError(resp); + }) as BulkError[]; + const successes = importTimelineResponse.filter((resp) => { + if (isImportRegular(resp)) { + return resp.status_code === 200; + } else { + return false; + } + }); + const timelinesInstalled = importTimelineResponse.filter( + (resp) => isImportRegular(resp) && resp.action === 'createViaImport' + ); + const timelinesUpdated = importTimelineResponse.filter( + (resp) => isImportRegular(resp) && resp.action === 'updateViaImport' + ); + const importTimelinesRes: ImportTimelineResultSchema = { + success: errorsResp.length === 0, + success_count: successes.length, + errors: errorsResp, + timelines_installed: timelinesInstalled.length ?? 0, + timelines_updated: timelinesUpdated.length ?? 0, + }; + const [validated, errors] = validate(importTimelinesRes, importTimelineResultSchema); + if (errors != null || validated == null) { + return new Error(errors || 'Import timeline error'); + } else { + return validated; + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/install_prepacked_timelines.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/install_prepacked_timelines.test.ts new file mode 100644 index 0000000000000..66f16db01a508 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/install_prepacked_timelines.test.ts @@ -0,0 +1,257 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { join, resolve } from 'path'; + +import { createPromiseFromStreams } from '../../../../../../../../src/legacy/utils/streams'; +import { SecurityPluginSetup } from '../../../../../../security/server'; + +import { FrameworkRequest } from '../../../framework'; +import { + createMockConfig, + requestContextMock, + mockGetCurrentUser, +} from '../../../detection_engine/routes/__mocks__'; +import { + addPrepackagedRulesRequest, + getNonEmptyIndex, + getFindResultWithSingleHit, +} from '../../../detection_engine/routes/__mocks__/request_responses'; + +import * as lib from './install_prepacked_timelines'; +import { importTimelines } from './import_timelines'; +import { buildFrameworkRequest } from './common'; +import { ImportTimelineResultSchema } from '../../../../../common/types/timeline'; + +jest.mock('./import_timelines'); + +describe('installPrepackagedTimelines', () => { + let securitySetup: SecurityPluginSetup; + let frameworkRequest: FrameworkRequest; + const spyInstallPrepackagedTimelines = jest.spyOn(lib, 'installPrepackagedTimelines'); + + const { clients, context } = requestContextMock.createTools(); + const config = createMockConfig(); + const mockFilePath = '../__mocks__'; + const mockFileName = 'prepackaged_timelines.ndjson'; + + beforeEach(async () => { + securitySetup = ({ + authc: { + getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), + }, + authz: {}, + } as unknown) as SecurityPluginSetup; + + clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + + jest.doMock('./install_prepacked_timelines', () => { + return { + ...lib, + installPrepackagedTimelines: spyInstallPrepackagedTimelines, + }; + }); + + const request = addPrepackagedRulesRequest(); + frameworkRequest = await buildFrameworkRequest(context, securitySetup, request); + }); + + afterEach(() => { + spyInstallPrepackagedTimelines.mockClear(); + }); + + afterAll(() => { + jest.resetModules(); + }); + + test('should call importTimelines', async () => { + await lib.installPrepackagedTimelines( + config.maxTimelineImportExportSize, + frameworkRequest, + true, + mockFilePath, + mockFileName + ); + + expect(importTimelines).toHaveBeenCalled(); + }); + + test('should call importTimelines with Readables', async () => { + const dir = resolve(join(__dirname, mockFilePath)); + const file = mockFileName; + await lib.installPrepackagedTimelines( + config.maxTimelineImportExportSize, + frameworkRequest, + true, + dir, + file + ); + const args = await createPromiseFromStreams([(importTimelines as jest.Mock).mock.calls[0][0]]); + const expected = JSON.stringify({ + savedObjectId: 'mocked-timeline-id-1', + version: 'WzExNzEyLDFd', + columns: [ + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: '@timestamp', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'signal.rule.description', + searchable: null, + }, + { + indexes: null, + aggregatable: true, + name: null, + description: + 'The action captured by the event.\n\nThis describes the information in the event. It is more specific than `event.category`.\nExamples are `group-add`, `process-started`, `file-created`. The value is\nnormally defined by the implementer.', + columnHeaderType: 'not-filtered', + id: 'event.action', + category: 'event', + type: 'string', + searchable: null, + example: 'user-password-change', + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'endgame.data.rule_name', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'rule.reference', + searchable: null, + }, + { + aggregatable: true, + description: + 'Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.', + columnHeaderType: 'not-filtered', + id: 'host.name', + category: 'host', + type: 'string', + }, + { + aggregatable: true, + description: 'Operating system name, without the version.', + columnHeaderType: 'not-filtered', + id: 'host.os.name', + category: 'host', + type: 'string', + example: 'Mac OS X', + }, + ], + dataProviders: [ + { + excluded: false, + and: [], + kqlQuery: '', + name: '3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509', + queryMatch: { + displayValue: null, + field: '_id', + displayField: null, + value: '3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509', + operator: ':', + }, + id: + 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509', + enabled: true, + }, + ], + description: '', + eventType: 'all', + filters: [], + kqlMode: 'filter', + kqlQuery: { filterQuery: { kuery: { kind: 'kuery', expression: '' }, serializedQuery: '' } }, + title: 'Generic Endpoint Timeline', + dateRange: { start: 1588257731065, end: 1588258391065 }, + savedQueryId: null, + sort: { columnId: '@timestamp', sortDirection: 'desc' }, + created: 1588258576517, + createdBy: 'elastic', + updated: 1588261039030, + updatedBy: 'elastic', + eventNotes: [], + globalNotes: [], + pinnedEventIds: [], + timelineType: 'template', + }); + expect(args).toEqual(expected); + }); + + test('should call importTimelines with maxTimelineImportExportSize', async () => { + const dir = resolve(join(__dirname, mockFilePath)); + const file = mockFileName; + await lib.installPrepackagedTimelines( + config.maxTimelineImportExportSize, + frameworkRequest, + true, + dir, + file + ); + + expect((importTimelines as jest.Mock).mock.calls[0][1]).toEqual( + config.maxTimelineImportExportSize + ); + }); + + test('should call importTimelines with frameworkRequest', async () => { + const dir = resolve(join(__dirname, mockFilePath)); + const file = mockFileName; + await lib.installPrepackagedTimelines( + config.maxTimelineImportExportSize, + frameworkRequest, + true, + dir, + file + ); + + expect(JSON.stringify((importTimelines as jest.Mock).mock.calls[0][2])).toEqual( + JSON.stringify(frameworkRequest) + ); + }); + + test('should call importTimelines with immutable', async () => { + const dir = resolve(join(__dirname, mockFilePath)); + const file = mockFileName; + await lib.installPrepackagedTimelines( + config.maxTimelineImportExportSize, + frameworkRequest, + true, + dir, + file + ); + + expect((importTimelines as jest.Mock).mock.calls[0][3]).toEqual(true); + }); + + test('should handle errors from getReadables', async () => { + const result = await lib.installPrepackagedTimelines( + config.maxTimelineImportExportSize, + frameworkRequest, + true, + mockFilePath, + 'prepackaged_timeline.ndjson' + ); + + expect( + (result as ImportTimelineResultSchema).errors[0].error.message.includes( + 'read prepackaged timelines error:' + ) + ).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/install_prepacked_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/install_prepacked_timelines.ts new file mode 100644 index 0000000000000..eb83a463eabbf --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/install_prepacked_timelines.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import path, { join, resolve } from 'path'; +import { Readable } from 'stream'; + +import { ImportTimelineResultSchema } from '../../../../../common/types/timeline'; + +import { FrameworkRequest } from '../../../framework'; + +import { importTimelines } from './import_timelines'; + +import { loadData, getReadables } from './common'; + +export const installPrepackagedTimelines = async ( + maxTimelineImportExportSize: number, + frameworkRequest: FrameworkRequest, + isImmutable: boolean, + filePath?: string, + fileName?: string +): Promise => { + let readStream; + const dir = resolve( + join(__dirname, filePath ?? '../../../detection_engine/rules/prepackaged_timelines') + ); + const file = fileName ?? 'index.ndjson'; + const dataPath = path.join(dir, file); + try { + readStream = await getReadables(dataPath); + } catch (err) { + return { + success: false, + success_count: 0, + timelines_installed: 0, + timelines_updated: 0, + errors: [ + { + error: { message: `read prepackaged timelines error: ${err.message}`, status_code: 500 }, + }, + ], + }; + } + + return loadData(readStream, (docs: T) => + docs instanceof Readable + ? importTimelines(docs, maxTimelineImportExportSize, frameworkRequest, isImmutable) + : Promise.reject(new Error(`read prepackaged timelines error`)) + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts index f4dbd2db3329c..82a2a866a71ff 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts @@ -14,6 +14,7 @@ import { SavedTimeline, TimelineSavedObject, TimelineTypeLiteralWithNull, + ExportTimelineNotFoundError, TimelineStatusLiteralWithNull, TemplateTimelineTypeLiteralWithNull, TemplateTimelineType, @@ -35,6 +36,7 @@ import { convertSavedObjectToSavedTimeline } from './convert_saved_object_to_sav import { pickSavedTimeline } from './pick_saved_timeline'; import { timelineSavedObjectType } from './saved_object_mappings'; import { draftTimelineDefaults } from './default_timeline'; +import { AuthenticatedUser } from '../../../../security/server'; interface ResponseTimelines { timeline: TimelineSavedObject[]; @@ -81,7 +83,7 @@ export interface Timeline { timelineId: string | null, version: string | null, timeline: SavedTimeline, - timelineType?: TimelineTypeLiteralWithNull + isImmutable?: boolean ) => Promise; deleteTimeline: (request: FrameworkRequest, timelineIds: string[]) => Promise; @@ -160,6 +162,33 @@ const getTimelineTypeFilter = ( return filters.filter((f) => f != null).join(' and '); }; +export const getExistingPrepackagedTimelines = async ( + request: FrameworkRequest, + countsOnly?: boolean, + pageInfo?: PageInfoTimeline | null +): Promise<{ + totalCount: number; + timeline: TimelineSavedObject[]; +}> => { + const queryPageInfo = countsOnly + ? { + perPage: 1, + page: 1, + } + : pageInfo ?? {}; + const elasticTemplateTimelineOptions = { + type: timelineSavedObjectType, + ...queryPageInfo, + filter: getTimelineTypeFilter( + TimelineType.template, + TemplateTimelineType.elastic, + TimelineStatus.immutable + ), + }; + + return getAllSavedTimeline(request, elasticTemplateTimelineOptions); +}; + export const getAllTimeline = async ( request: FrameworkRequest, onlyUserFavorite: boolean | null, @@ -172,8 +201,8 @@ export const getAllTimeline = async ( ): Promise => { const options: SavedObjectsFindOptions = { type: timelineSavedObjectType, - perPage: pageInfo != null ? pageInfo.pageSize : undefined, - page: pageInfo != null ? pageInfo.pageIndex : undefined, + perPage: pageInfo?.pageSize ?? undefined, + page: pageInfo?.pageIndex ?? undefined, search: search != null ? search : undefined, searchFields: onlyUserFavorite ? ['title', 'description', 'favorite.keySearch'] @@ -197,17 +226,6 @@ export const getAllTimeline = async ( filter: getTimelineTypeFilter(TimelineType.template, null, null), }; - const elasticTemplateTimelineOptions = { - type: timelineSavedObjectType, - perPage: 1, - page: 1, - filter: getTimelineTypeFilter( - TimelineType.template, - TemplateTimelineType.elastic, - TimelineStatus.immutable - ), - }; - const customTemplateTimelineOptions = { type: timelineSavedObjectType, perPage: 1, @@ -231,7 +249,7 @@ export const getAllTimeline = async ( getAllSavedTimeline(request, options), getAllSavedTimeline(request, timelineOptions), getAllSavedTimeline(request, templateTimelineOptions), - getAllSavedTimeline(request, elasticTemplateTimelineOptions), + getExistingPrepackagedTimelines(request, true), getAllSavedTimeline(request, customTemplateTimelineOptions), getAllSavedTimeline(request, favoriteTimelineOptions), ]); @@ -336,16 +354,18 @@ export const persistTimeline = async ( request: FrameworkRequest, timelineId: string | null, version: string | null, - timeline: SavedTimeline + timeline: SavedTimeline, + isImmutable?: boolean ): Promise => { const savedObjectsClient = request.context.core.savedObjects.client; + const userInfo = isImmutable ? ({ username: 'Elastic' } as AuthenticatedUser) : request.user; try { if (timelineId == null) { // Create new timeline const newTimeline = convertSavedObjectToSavedTimeline( await savedObjectsClient.create( timelineSavedObjectType, - pickSavedTimeline(timelineId, timeline, request.user) + pickSavedTimeline(timelineId, timeline, userInfo) ) ); return { @@ -358,7 +378,7 @@ export const persistTimeline = async ( await savedObjectsClient.update( timelineSavedObjectType, timelineId, - pickSavedTimeline(timelineId, timeline, request.user), + pickSavedTimeline(timelineId, timeline, userInfo), { version: version || undefined, } @@ -537,3 +557,50 @@ export const timelineWithReduxProperties = ( pinnedEventIds: pinnedEvents.map((e) => e.eventId), pinnedEventsSaveObject: pinnedEvents, }); + +export const getTimelines = async (request: FrameworkRequest, timelineIds?: string[] | null) => { + const savedObjectsClient = request.context.core.savedObjects.client; + let exportedIds = timelineIds; + if (timelineIds == null || timelineIds.length === 0) { + const { timeline: savedAllTimelines } = await getAllTimeline( + request, + false, + null, + null, + null, + TimelineStatus.active, + null, + null + ); + exportedIds = savedAllTimelines.map((t) => t.savedObjectId); + } + + const savedObjects = await Promise.resolve( + savedObjectsClient.bulkGet( + exportedIds?.reduce( + (acc, timelineId) => [...acc, { id: timelineId, type: timelineSavedObjectType }], + [] as Array<{ id: string; type: string }> + ) + ) + ); + + const timelineObjects: { + timelines: TimelineSavedObject[]; + errors: ExportTimelineNotFoundError[]; + } = savedObjects.saved_objects.reduce( + (acc, savedObject) => { + return savedObject.error == null + ? { + errors: acc.errors, + timelines: [...acc.timelines, convertSavedObjectToSavedTimeline(savedObject)], + } + : { errors: [...acc.errors, savedObject.error], timelines: acc.timelines }; + }, + { + timelines: [] as TimelineSavedObject[], + errors: [] as ExportTimelineNotFoundError[], + } + ); + + return timelineObjects; +}; diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index 54d7dcccba815..37a97c03ad332 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -36,6 +36,8 @@ import { getDraftTimelinesRoute } from '../lib/timeline/routes/get_draft_timelin import { cleanDraftTimelinesRoute } from '../lib/timeline/routes/clean_draft_timelines_route'; import { SetupPlugins } from '../plugin'; import { ConfigType } from '../config'; +import { installPrepackedTimelinesRoute } from '../lib/timeline/routes/install_prepacked_timelines_route'; +import { getTimelineByIdRoute } from '../lib/timeline/routes/get_timeline_by_id_route'; export const initRoutes = ( router: IRouter, @@ -53,8 +55,8 @@ export const initRoutes = ( deleteRulesRoute(router); findRulesRoute(router); - addPrepackedRulesRoute(router); - getPrepackagedRulesStatusRoute(router); + addPrepackedRulesRoute(router, config, security); + getPrepackagedRulesStatusRoute(router, config, security); createRulesBulkRoute(router, ml); updateRulesBulkRoute(router, ml); patchRulesBulkRoute(router, ml); @@ -66,10 +68,13 @@ export const initRoutes = ( exportRulesRoute(router, config); importTimelinesRoute(router, config, security); - exportTimelinesRoute(router, config); + exportTimelinesRoute(router, config, security); getDraftTimelinesRoute(router, config, security); + getTimelineByIdRoute(router, config, security); cleanDraftTimelinesRoute(router, config, security); + installPrepackedTimelinesRoute(router, config, security); + findRulesStatusesRoute(router); // Detection Engine Signals routes that have the REST endpoints of /api/detection_engine/signals diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 7191c88cab49d..c8fe792af926d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -13614,9 +13614,6 @@ "xpack.securitySolution.containers.case.reopenedCases": "{totalCases, plural, =1 {\"{caseTitle}\"} other {{totalCases}件のケース}}を再オープンしました", "xpack.securitySolution.containers.case.updatedCase": "\"{caseTitle}\"を更新しました", "xpack.securitySolution.containers.detectionEngine.addRuleFailDescription": "ルールを追加できませんでした", - "xpack.securitySolution.containers.detectionEngine.createPrePackagedRuleFailDescription": "Elasticから事前にパッケージ化されているルールをインストールすることができませんでした", - "xpack.securitySolution.containers.detectionEngine.createPrePackagedRuleSuccesDescription": "Elasticから事前にパッケージ化されているルールをインストールしました", - "xpack.securitySolution.containers.detectionEngine.rules": "ルールを取得できませんでした", "xpack.securitySolution.containers.detectionEngine.tagFetchFailDescription": "タグを取得できませんでした", "xpack.securitySolution.containers.errors.dataFetchFailureTitle": "データの取得に失敗", "xpack.securitySolution.containers.errors.networkFailureTitle": "ネットワーク障害", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b4502781ddbb1..7640675a427ce 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -13620,9 +13620,6 @@ "xpack.securitySolution.containers.case.reopenedCases": "已重新打开 {totalCases, plural, =1 {\"{caseTitle}\"} other {{totalCases} 个案例}}", "xpack.securitySolution.containers.case.updatedCase": "已更新“{caseTitle}”", "xpack.securitySolution.containers.detectionEngine.addRuleFailDescription": "无法添加规则", - "xpack.securitySolution.containers.detectionEngine.createPrePackagedRuleFailDescription": "无法安装 elastic 的预打包规则", - "xpack.securitySolution.containers.detectionEngine.createPrePackagedRuleSuccesDescription": "已安装 elastic 的预打包规则", - "xpack.securitySolution.containers.detectionEngine.rules": "无法提取规则", "xpack.securitySolution.containers.detectionEngine.tagFetchFailDescription": "无法提取标记", "xpack.securitySolution.containers.errors.dataFetchFailureTitle": "数据提取失败", "xpack.securitySolution.containers.errors.networkFailureTitle": "网络故障", diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts index 04564fc00ec2f..242f906d0d197 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts @@ -8,7 +8,12 @@ import expect from '@kbn/expect'; import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { createSignalsIndex, deleteAllAlerts, deleteSignalsIndex } from '../../utils'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteAllTimelines, + deleteSignalsIndex, +} from '../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -40,6 +45,7 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(async () => { await deleteSignalsIndex(supertest); await deleteAllAlerts(es); + await deleteAllTimelines(es); }); it('should contain two output keys of rules_installed and rules_updated', async () => { @@ -49,7 +55,12 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(200); - expect(Object.keys(body)).to.eql(['rules_installed', 'rules_updated']); + expect(Object.keys(body)).to.eql([ + 'rules_installed', + 'rules_updated', + 'timelines_installed', + 'timelines_updated', + ]); }); it('should create the prepackaged rules and return a count greater than zero', async () => { diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/get_prepackaged_rules_status.ts b/x-pack/test/detection_engine_api_integration/basic/tests/get_prepackaged_rules_status.ts index 8af67f818ea91..1bbfce42d2baa 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/get_prepackaged_rules_status.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/get_prepackaged_rules_status.ts @@ -16,6 +16,7 @@ import { deleteAllAlerts, deleteSignalsIndex, getSimpleRule, + deleteAllTimelines, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -32,9 +33,10 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(async () => { await deleteSignalsIndex(supertest); await deleteAllAlerts(es); + await deleteAllTimelines(es); }); - it('should return expected JSON keys of the pre-packaged rules status', async () => { + it('should return expected JSON keys of the pre-packaged rules and pre-packaged timelines status', async () => { const { body } = await supertest .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) .set('kbn-xsrf', 'true') @@ -46,6 +48,9 @@ export default ({ getService }: FtrProviderContext): void => { 'rules_installed', 'rules_not_installed', 'rules_not_updated', + 'timelines_installed', + 'timelines_not_installed', + 'timelines_not_updated', ]); }); @@ -58,6 +63,15 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.rules_not_installed).to.be.greaterThan(0); }); + it('should return that timelines_not_installed are greater than zero', async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.timelines_not_installed).to.be.greaterThan(0); + }); + it('should return that rules_custom_installed, rules_installed, and rules_not_updated are zero', async () => { const { body } = await supertest .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) @@ -69,6 +83,16 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.rules_not_updated).to.eql(0); }); + it('should return that timelines_installed, and timelines_not_updated are zero', async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.timelines_installed).to.eql(0); + expect(body.timelines_not_updated).to.eql(0); + }); + it('should show that one custom rule is installed when a custom rule is added', async () => { await supertest .post(DETECTION_ENGINE_RULES_URL) @@ -84,9 +108,11 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.rules_custom_installed).to.eql(1); expect(body.rules_installed).to.eql(0); expect(body.rules_not_updated).to.eql(0); + expect(body.timelines_installed).to.eql(0); + expect(body.timelines_not_updated).to.eql(0); }); - it('should show rules are installed when adding pre-packaged rules', async () => { + it('should show rules and timelines are installed when adding pre-packaged rules', async () => { await supertest .put(DETECTION_ENGINE_PREPACKAGED_URL) .set('kbn-xsrf', 'true') @@ -99,6 +125,7 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(200); expect(body.rules_installed).to.be.greaterThan(0); + expect(body.timelines_installed).to.be.greaterThan(0); }); }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts index 04564fc00ec2f..242f906d0d197 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts @@ -8,7 +8,12 @@ import expect from '@kbn/expect'; import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { createSignalsIndex, deleteAllAlerts, deleteSignalsIndex } from '../../utils'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteAllTimelines, + deleteSignalsIndex, +} from '../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -40,6 +45,7 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(async () => { await deleteSignalsIndex(supertest); await deleteAllAlerts(es); + await deleteAllTimelines(es); }); it('should contain two output keys of rules_installed and rules_updated', async () => { @@ -49,7 +55,12 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(200); - expect(Object.keys(body)).to.eql(['rules_installed', 'rules_updated']); + expect(Object.keys(body)).to.eql([ + 'rules_installed', + 'rules_updated', + 'timelines_installed', + 'timelines_updated', + ]); }); it('should create the prepackaged rules and return a count greater than zero', async () => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts index 8af67f818ea91..1bbfce42d2baa 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts @@ -16,6 +16,7 @@ import { deleteAllAlerts, deleteSignalsIndex, getSimpleRule, + deleteAllTimelines, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -32,9 +33,10 @@ export default ({ getService }: FtrProviderContext): void => { afterEach(async () => { await deleteSignalsIndex(supertest); await deleteAllAlerts(es); + await deleteAllTimelines(es); }); - it('should return expected JSON keys of the pre-packaged rules status', async () => { + it('should return expected JSON keys of the pre-packaged rules and pre-packaged timelines status', async () => { const { body } = await supertest .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) .set('kbn-xsrf', 'true') @@ -46,6 +48,9 @@ export default ({ getService }: FtrProviderContext): void => { 'rules_installed', 'rules_not_installed', 'rules_not_updated', + 'timelines_installed', + 'timelines_not_installed', + 'timelines_not_updated', ]); }); @@ -58,6 +63,15 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.rules_not_installed).to.be.greaterThan(0); }); + it('should return that timelines_not_installed are greater than zero', async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.timelines_not_installed).to.be.greaterThan(0); + }); + it('should return that rules_custom_installed, rules_installed, and rules_not_updated are zero', async () => { const { body } = await supertest .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) @@ -69,6 +83,16 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.rules_not_updated).to.eql(0); }); + it('should return that timelines_installed, and timelines_not_updated are zero', async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.timelines_installed).to.eql(0); + expect(body.timelines_not_updated).to.eql(0); + }); + it('should show that one custom rule is installed when a custom rule is added', async () => { await supertest .post(DETECTION_ENGINE_RULES_URL) @@ -84,9 +108,11 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.rules_custom_installed).to.eql(1); expect(body.rules_installed).to.eql(0); expect(body.rules_not_updated).to.eql(0); + expect(body.timelines_installed).to.eql(0); + expect(body.timelines_not_updated).to.eql(0); }); - it('should show rules are installed when adding pre-packaged rules', async () => { + it('should show rules and timelines are installed when adding pre-packaged rules', async () => { await supertest .put(DETECTION_ENGINE_PREPACKAGED_URL) .set('kbn-xsrf', 'true') @@ -99,6 +125,7 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(200); expect(body.rules_installed).to.be.greaterThan(0); + expect(body.timelines_installed).to.be.greaterThan(0); }); }); }); diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 4b980536d2cd1..102a1577a7eaf 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -259,6 +259,20 @@ export const deleteAllAlerts = async (es: Client, retryCount = 20): Promise => { + await es.deleteByQuery({ + index: '.kibana', + q: 'type:siem-ui-timeline', + wait_for_completion: true, + refresh: true, + body: {}, + }); +}; + /** * Remove all rules statuses from the .kibana index * This will retry 20 times before giving up and hopefully still not interfere with other tests From 01f021daa15cfca246a49a7c56aaddd7ed2b74e3 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 14 Jul 2020 18:18:57 +0200 Subject: [PATCH 12/82] updates exception empty prompt text (#71654) --- .../public/common/components/exceptions/translations.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts index 03beee8ab373e..ee3255446b334 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts @@ -91,7 +91,7 @@ export const ADD_TO_DETECTIONS_LIST = i18n.translate( export const EXCEPTION_EMPTY_PROMPT_TITLE = i18n.translate( 'xpack.securitySolution.exceptions.viewer.emptyPromptTitle', { - defaultMessage: 'You have no exceptions', + defaultMessage: 'This rule has no exceptions', } ); @@ -99,7 +99,7 @@ export const EXCEPTION_EMPTY_PROMPT_BODY = i18n.translate( 'xpack.securitySolution.exceptions.viewer.emptyPromptBody', { defaultMessage: - 'You can add an exception to fine tune the rule so that it suppresses alerts that meet specified conditions. Exceptions leverage detection accuracy, which can help reduce the number of false positives.', + 'You can add exceptions to fine tune the rule so that detection alerts are not created when exception conditions are met. Exceptions improve detection accuracy, which can help reduce the number of false positives.', } ); From b26e3198b3c75266820b4b69f3bd5b86565c8bbb Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Tue, 14 Jul 2020 11:30:01 -0500 Subject: [PATCH 13/82] Management API - simpler interface, remove app context usage (#71144) Management API - simpler interface, remove app context usage, consolidate rendeing --- src/core/MIGRATION.md | 2 +- .../advanced_settings/public/plugin.ts | 3 +- .../index_pattern_management/public/plugin.ts | 4 +- src/plugins/management/kibana.json | 2 +- src/plugins/management/public/application.tsx | 10 +- .../management/public/components/index.ts | 1 - .../management_app/management_app.tsx | 28 ++-- .../public/components/management_sections.tsx | 145 ++++++++++++------ .../management_sidebar_nav.tsx | 29 +++- src/plugins/management/public/index.ts | 2 +- .../management_sections_service.test.ts | 26 ++-- .../public/management_sections_service.ts | 57 ++++--- src/plugins/management/public/mocks/index.ts | 25 +-- src/plugins/management/public/plugin.ts | 20 ++- src/plugins/management/public/types.ts | 24 ++- .../public/utils/management_item.ts | 7 +- .../saved_objects_management/public/plugin.ts | 4 +- .../management_test_plugin/public/plugin.tsx | 4 +- .../framework/kibana_framework_adapter.ts | 3 +- .../public/plugin.ts | 3 +- .../public/plugin.tsx | 3 +- .../plugins/index_management/public/plugin.ts | 4 +- .../plugins/ingest_pipelines/public/plugin.ts | 3 +- .../license_management/public/plugin.ts | 4 +- x-pack/plugins/logstash/public/plugin.ts | 30 ++-- x-pack/plugins/ml/kibana.json | 1 - .../ml/public/application/management/index.ts | 7 +- .../plugins/remote_clusters/public/plugin.ts | 3 +- x-pack/plugins/reporting/public/plugin.tsx | 5 +- x-pack/plugins/rollup/public/plugin.ts | 4 +- x-pack/plugins/security/kibana.json | 4 +- .../management/management_service.test.ts | 32 ++-- .../public/management/management_service.ts | 23 ++- .../plugins/security/public/plugin.test.tsx | 5 +- x-pack/plugins/security/public/plugin.tsx | 3 +- .../plugins/snapshot_restore/public/plugin.ts | 4 +- x-pack/plugins/spaces/kibana.json | 1 - .../management/management_service.test.ts | 18 +-- .../public/management/management_service.tsx | 14 +- x-pack/plugins/spaces/public/plugin.test.ts | 24 ++- x-pack/plugins/transform/public/plugin.ts | 4 +- .../triggers_actions_ui/public/plugin.ts | 94 ++++++++---- x-pack/plugins/upgrade_assistant/kibana.json | 3 +- .../upgrade_assistant/public/plugin.ts | 4 +- x-pack/plugins/watcher/public/plugin.ts | 3 +- 45 files changed, 388 insertions(+), 311 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 5757b6dff8d3f..f7acff14915a7 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -942,7 +942,7 @@ export class MyPlugin implements Plugin { return mountApp(await core.getStartServices(), params); }, }); - plugins.management.sections.getSection('another').registerApp({ + plugins.management.sections.section.kibana.registerApp({ id: 'app', title: 'My app', order: 1, diff --git a/src/plugins/advanced_settings/public/plugin.ts b/src/plugins/advanced_settings/public/plugin.ts index 8b3347f8d88f0..35f6dd65925ba 100644 --- a/src/plugins/advanced_settings/public/plugin.ts +++ b/src/plugins/advanced_settings/public/plugin.ts @@ -18,7 +18,6 @@ */ import { i18n } from '@kbn/i18n'; import { CoreSetup, Plugin } from 'kibana/public'; -import { ManagementSectionId } from '../../management/public'; import { ComponentRegistry } from './component_registry'; import { AdvancedSettingsSetup, AdvancedSettingsStart, AdvancedSettingsPluginSetup } from './types'; @@ -31,7 +30,7 @@ const title = i18n.translate('advancedSettings.advancedSettingsLabel', { export class AdvancedSettingsPlugin implements Plugin { public setup(core: CoreSetup, { management }: AdvancedSettingsPluginSetup) { - const kibanaSection = management.sections.getSection(ManagementSectionId.Kibana); + const kibanaSection = management.sections.section.kibana; kibanaSection.registerApp({ id: 'settings', diff --git a/src/plugins/index_pattern_management/public/plugin.ts b/src/plugins/index_pattern_management/public/plugin.ts index 6e93d23f8469c..fe680eff8657e 100644 --- a/src/plugins/index_pattern_management/public/plugin.ts +++ b/src/plugins/index_pattern_management/public/plugin.ts @@ -27,7 +27,7 @@ import { IndexPatternManagementServiceStart, } from './service'; -import { ManagementSetup, ManagementSectionId } from '../../management/public'; +import { ManagementSetup } from '../../management/public'; export interface IndexPatternManagementSetupDependencies { management: ManagementSetup; @@ -64,7 +64,7 @@ export class IndexPatternManagementPlugin core: CoreSetup, { management, kibanaLegacy }: IndexPatternManagementSetupDependencies ) { - const kibanaSection = management.sections.getSection(ManagementSectionId.Kibana); + const kibanaSection = management.sections.section.kibana; if (!kibanaSection) { throw new Error('`kibana` management section not found.'); diff --git a/src/plugins/management/kibana.json b/src/plugins/management/kibana.json index f48158e98ff3f..308e006b5aba0 100644 --- a/src/plugins/management/kibana.json +++ b/src/plugins/management/kibana.json @@ -4,5 +4,5 @@ "server": true, "ui": true, "requiredPlugins": ["kibanaLegacy", "home"], - "requiredBundles": ["kibanaReact"] + "requiredBundles": ["kibanaReact", "kibanaUtils"] } diff --git a/src/plugins/management/public/application.tsx b/src/plugins/management/public/application.tsx index 5d014504b8938..035f5d56e4cc7 100644 --- a/src/plugins/management/public/application.tsx +++ b/src/plugins/management/public/application.tsx @@ -20,21 +20,15 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { AppMountContext, AppMountParameters } from 'kibana/public'; +import { AppMountParameters } from 'kibana/public'; import { ManagementApp, ManagementAppDependencies } from './components/management_app'; export const renderApp = async ( - context: AppMountContext, { history, appBasePath, element }: AppMountParameters, dependencies: ManagementAppDependencies ) => { ReactDOM.render( - , + , element ); diff --git a/src/plugins/management/public/components/index.ts b/src/plugins/management/public/components/index.ts index 8979809c5245e..3a2a3eafb89e2 100644 --- a/src/plugins/management/public/components/index.ts +++ b/src/plugins/management/public/components/index.ts @@ -18,4 +18,3 @@ */ export { ManagementApp } from './management_app'; -export { managementSections } from './management_sections'; diff --git a/src/plugins/management/public/components/management_app/management_app.tsx b/src/plugins/management/public/components/management_app/management_app.tsx index fc5a8924c95d6..313884a90908f 100644 --- a/src/plugins/management/public/components/management_app/management_app.tsx +++ b/src/plugins/management/public/components/management_app/management_app.tsx @@ -17,36 +17,32 @@ * under the License. */ import React, { useState, useEffect, useCallback } from 'react'; -import { - AppMountContext, - AppMountParameters, - ChromeBreadcrumb, - ScopedHistory, -} from 'kibana/public'; +import { AppMountParameters, ChromeBreadcrumb, ScopedHistory } from 'kibana/public'; import { I18nProvider } from '@kbn/i18n/react'; import { EuiPage } from '@elastic/eui'; -import { ManagementStart } from '../../types'; import { ManagementSection, MANAGEMENT_BREADCRUMB } from '../../utils'; import { ManagementRouter } from './management_router'; import { ManagementSidebarNav } from '../management_sidebar_nav'; import { reactRouterNavigate } from '../../../../kibana_react/public'; +import { SectionsServiceStart } from '../../types'; import './management_app.scss'; interface ManagementAppProps { appBasePath: string; - context: AppMountContext; history: AppMountParameters['history']; dependencies: ManagementAppDependencies; } export interface ManagementAppDependencies { - management: ManagementStart; + sections: SectionsServiceStart; kibanaVersion: string; + setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => void; } -export const ManagementApp = ({ context, dependencies, history }: ManagementAppProps) => { +export const ManagementApp = ({ dependencies, history }: ManagementAppProps) => { + const { setBreadcrumbs } = dependencies; const [selectedId, setSelectedId] = useState(''); const [sections, setSections] = useState(); @@ -55,24 +51,24 @@ export const ManagementApp = ({ context, dependencies, history }: ManagementAppP window.scrollTo(0, 0); }, []); - const setBreadcrumbs = useCallback( + const setBreadcrumbsScoped = useCallback( (crumbs: ChromeBreadcrumb[] = [], appHistory?: ScopedHistory) => { const wrapBreadcrumb = (item: ChromeBreadcrumb, scopedHistory: ScopedHistory) => ({ ...item, ...(item.href ? reactRouterNavigate(scopedHistory, item.href) : {}), }); - context.core.chrome.setBreadcrumbs([ + setBreadcrumbs([ wrapBreadcrumb(MANAGEMENT_BREADCRUMB, history), ...crumbs.map((item) => wrapBreadcrumb(item, appHistory || history)), ]); }, - [context.core.chrome, history] + [setBreadcrumbs, history] ); useEffect(() => { - setSections(dependencies.management.sections.getSectionsEnabled()); - }, [dependencies.management.sections]); + setSections(dependencies.sections.getSectionsEnabled()); + }, [dependencies.sections]); if (!sections) { return null; @@ -84,7 +80,7 @@ export const ManagementApp = ({ context, dependencies, history }: ManagementAppP ( - - - {text} +export const KibanaSection = { + id: ManagementSectionId.Kibana, + title: kibanaTitle, + tip: kibanaTip, + order: 4, +}; - - - - - -); +export const StackSection = { + id: ManagementSectionId.Stack, + title: stackTitle, + tip: stackTip, + order: 4, +}; export const managementSections = [ - { - id: ManagementSectionId.Ingest, - title: ( - - ), - }, - { - id: ManagementSectionId.Data, - title: , - }, - { - id: ManagementSectionId.InsightsAndAlerting, - title: ( - - ), - }, - { - id: ManagementSectionId.Security, - title: , - }, - { - id: ManagementSectionId.Kibana, - title: , - }, - { - id: ManagementSectionId.Stack, - title: , - }, + IngestSection, + DataSection, + InsightsAndAlertingSection, + SecuritySection, + KibanaSection, + StackSection, ]; diff --git a/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx b/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx index 055dda5ed84a1..37d1167661d82 100644 --- a/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx +++ b/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx @@ -21,7 +21,15 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { sortBy } from 'lodash'; -import { EuiIcon, EuiSideNav, EuiScreenReaderOnly, EuiSideNavItemType } from '@elastic/eui'; +import { + EuiIcon, + EuiSideNav, + EuiScreenReaderOnly, + EuiSideNavItemType, + EuiFlexGroup, + EuiFlexItem, + EuiToolTip, +} from '@elastic/eui'; import { AppMountParameters } from 'kibana/public'; import { ManagementApp, ManagementSection } from '../../utils'; @@ -79,6 +87,23 @@ export const ManagementSidebarNav = ({ }), })); + interface TooltipWrapperProps { + text: string; + tip?: string; + } + + const TooltipWrapper = ({ text, tip }: TooltipWrapperProps) => ( + + + {text} + + + + + + + ); + const createNavItem = ( item: T, customParams: Partial> = {} @@ -87,7 +112,7 @@ export const ManagementSidebarNav = ({ return { id: item.id, - name: item.title, + name: item.tip ? : item.title, isSelected: item.id === selectedId, icon: iconType ? : undefined, 'data-test-subj': item.id, diff --git a/src/plugins/management/public/index.ts b/src/plugins/management/public/index.ts index 3ba469c7831f6..f6c23ccf0143f 100644 --- a/src/plugins/management/public/index.ts +++ b/src/plugins/management/public/index.ts @@ -27,8 +27,8 @@ export function plugin(initializerContext: PluginInitializerContext) { export { RegisterManagementAppArgs, ManagementSection, ManagementApp } from './utils'; export { - ManagementSectionId, ManagementAppMountParams, ManagementSetup, ManagementStart, + DefinedSections, } from './types'; diff --git a/src/plugins/management/public/management_sections_service.test.ts b/src/plugins/management/public/management_sections_service.test.ts index fd56dd8a6ee27..3e0001e4ca550 100644 --- a/src/plugins/management/public/management_sections_service.test.ts +++ b/src/plugins/management/public/management_sections_service.test.ts @@ -17,8 +17,10 @@ * under the License. */ -import { ManagementSectionId } from './index'; -import { ManagementSectionsService } from './management_sections_service'; +import { + ManagementSectionsService, + getSectionsServiceStartPrivate, +} from './management_sections_service'; describe('ManagementService', () => { let managementService: ManagementSectionsService; @@ -35,15 +37,10 @@ describe('ManagementService', () => { test('Provides default sections', () => { managementService.setup(); - const start = managementService.start({ capabilities }); - - expect(start.getAllSections().length).toEqual(6); - expect(start.getSection(ManagementSectionId.Ingest)).toBeDefined(); - expect(start.getSection(ManagementSectionId.Data)).toBeDefined(); - expect(start.getSection(ManagementSectionId.InsightsAndAlerting)).toBeDefined(); - expect(start.getSection(ManagementSectionId.Security)).toBeDefined(); - expect(start.getSection(ManagementSectionId.Kibana)).toBeDefined(); - expect(start.getSection(ManagementSectionId.Stack)).toBeDefined(); + managementService.start({ capabilities }); + const start = getSectionsServiceStartPrivate(); + + expect(start.getSectionsEnabled().length).toEqual(6); }); test('Register section, enable and disable', () => { @@ -51,10 +48,11 @@ describe('ManagementService', () => { const setup = managementService.setup(); const testSection = setup.register({ id: 'test-section', title: 'Test Section' }); - expect(setup.getSection('test-section')).not.toBeUndefined(); + expect(testSection).not.toBeUndefined(); // Start phase: - const start = managementService.start({ capabilities }); + managementService.start({ capabilities }); + const start = getSectionsServiceStartPrivate(); expect(start.getSectionsEnabled().length).toEqual(7); @@ -71,7 +69,7 @@ describe('ManagementService', () => { testSection.registerApp({ id: 'test-app-2', title: 'Test App 2', mount: jest.fn() }); testSection.registerApp({ id: 'test-app-3', title: 'Test App 3', mount: jest.fn() }); - expect(setup.getSection('test-section')).not.toBeUndefined(); + expect(testSection).not.toBeUndefined(); // Start phase: managementService.start({ diff --git a/src/plugins/management/public/management_sections_service.ts b/src/plugins/management/public/management_sections_service.ts index d8d148a9247ff..b9dc2dd416d9a 100644 --- a/src/plugins/management/public/management_sections_service.ts +++ b/src/plugins/management/public/management_sections_service.ts @@ -17,22 +17,47 @@ * under the License. */ -import { ReactElement } from 'react'; import { ManagementSection, RegisterManagementSectionArgs } from './utils'; -import { managementSections } from './components/management_sections'; +import { + IngestSection, + DataSection, + InsightsAndAlertingSection, + SecuritySection, + KibanaSection, + StackSection, +} from './components/management_sections'; import { ManagementSectionId, SectionsServiceSetup, - SectionsServiceStart, SectionsServiceStartDeps, + DefinedSections, + ManagementSectionsStartPrivate, } from './types'; +import { createGetterSetter } from '../../kibana_utils/public'; + +const [getSectionsServiceStartPrivate, setSectionsServiceStartPrivate] = createGetterSetter< + ManagementSectionsStartPrivate +>('SectionsServiceStartPrivate'); + +export { getSectionsServiceStartPrivate }; export class ManagementSectionsService { - private sections: Map = new Map(); + definedSections: DefinedSections; - private getSection = (sectionId: ManagementSectionId | string) => - this.sections.get(sectionId) as ManagementSection; + constructor() { + // Note on adding sections - sections can be defined in a plugin and exported as a contract + // It is not necessary to define all sections here, although we've chose to do it for discovery reasons. + this.definedSections = { + ingest: this.registerSection(IngestSection), + data: this.registerSection(DataSection), + insightsAndAlerting: this.registerSection(InsightsAndAlertingSection), + security: this.registerSection(SecuritySection), + kibana: this.registerSection(KibanaSection), + stack: this.registerSection(StackSection), + }; + } + private sections: Map = new Map(); private getAllSections = () => [...this.sections.values()]; @@ -48,19 +73,15 @@ export class ManagementSectionsService { }; setup(): SectionsServiceSetup { - managementSections.forEach( - ({ id, title }: { id: ManagementSectionId; title: ReactElement }, idx: number) => { - this.registerSection({ id, title, order: idx }); - } - ); - return { register: this.registerSection, - getSection: this.getSection, + section: { + ...this.definedSections, + }, }; } - start({ capabilities }: SectionsServiceStartDeps): SectionsServiceStart { + start({ capabilities }: SectionsServiceStartDeps) { this.getAllSections().forEach((section) => { if (capabilities.management.hasOwnProperty(section.id)) { const sectionCapabilities = capabilities.management[section.id]; @@ -72,10 +93,10 @@ export class ManagementSectionsService { } }); - return { - getSection: this.getSection, - getAllSections: this.getAllSections, + setSectionsServiceStartPrivate({ getSectionsEnabled: () => this.getAllSections().filter((section) => section.enabled), - }; + }); + + return {}; } } diff --git a/src/plugins/management/public/mocks/index.ts b/src/plugins/management/public/mocks/index.ts index 123e3f28877aa..fbb37647dad90 100644 --- a/src/plugins/management/public/mocks/index.ts +++ b/src/plugins/management/public/mocks/index.ts @@ -17,10 +17,10 @@ * under the License. */ -import { ManagementSetup, ManagementStart } from '../types'; +import { ManagementSetup, ManagementStart, DefinedSections } from '../types'; import { ManagementSection } from '../index'; -const createManagementSectionMock = () => +export const createManagementSectionMock = () => (({ disable: jest.fn(), enable: jest.fn(), @@ -29,19 +29,22 @@ const createManagementSectionMock = () => getEnabledItems: jest.fn().mockReturnValue([]), } as unknown) as ManagementSection); -const createSetupContract = (): DeeplyMockedKeys => ({ +const createSetupContract = (): ManagementSetup => ({ sections: { - register: jest.fn(), - getSection: jest.fn().mockReturnValue(createManagementSectionMock()), + register: jest.fn(() => createManagementSectionMock()), + section: ({ + ingest: createManagementSectionMock(), + data: createManagementSectionMock(), + insightsAndAlerting: createManagementSectionMock(), + security: createManagementSectionMock(), + kibana: createManagementSectionMock(), + stack: createManagementSectionMock(), + } as unknown) as DefinedSections, }, }); -const createStartContract = (): DeeplyMockedKeys => ({ - sections: { - getSection: jest.fn(), - getAllSections: jest.fn(), - getSectionsEnabled: jest.fn(), - }, +const createStartContract = (): ManagementStart => ({ + sections: {}, }); export const managementPluginMock = { diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index dada4636e6add..17d8cb4adc701 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -26,9 +26,13 @@ import { Plugin, DEFAULT_APP_CATEGORIES, PluginInitializerContext, + AppMountParameters, } from '../../../core/public'; -import { ManagementSectionsService } from './management_sections_service'; +import { + ManagementSectionsService, + getSectionsServiceStartPrivate, +} from './management_sections_service'; interface ManagementSetupDependencies { home: HomePublicPluginSetup; @@ -64,13 +68,14 @@ export class ManagementPlugin implements Plugin ManagementSection[]; } export interface SectionsServiceStartDeps { @@ -36,12 +47,10 @@ export interface SectionsServiceStartDeps { export interface SectionsServiceSetup { register: (args: Omit) => ManagementSection; - getSection: (sectionId: ManagementSectionId | string) => ManagementSection; + section: DefinedSections; } export interface SectionsServiceStart { - getSection: (sectionId: ManagementSectionId | string) => ManagementSection; - getAllSections: () => ManagementSection[]; getSectionsEnabled: () => ManagementSection[]; } @@ -66,7 +75,8 @@ export interface ManagementAppMountParams { export interface CreateManagementItemArgs { id: string; - title: string | ReactElement; + title: string; + tip?: string; order?: number; euiIconType?: string; // takes precedence over `icon` property. icon?: string; // URL to image file; fallback if no `euiIconType` diff --git a/src/plugins/management/public/utils/management_item.ts b/src/plugins/management/public/utils/management_item.ts index ef0c8e4693895..e6e473c77bf61 100644 --- a/src/plugins/management/public/utils/management_item.ts +++ b/src/plugins/management/public/utils/management_item.ts @@ -16,21 +16,22 @@ * specific language governing permissions and limitations * under the License. */ -import { ReactElement } from 'react'; import { CreateManagementItemArgs } from '../types'; export class ManagementItem { public readonly id: string = ''; - public readonly title: string | ReactElement = ''; + public readonly title: string; + public readonly tip?: string; public readonly order: number; public readonly euiIconType?: string; public readonly icon?: string; public enabled: boolean = true; - constructor({ id, title, order = 100, euiIconType, icon }: CreateManagementItemArgs) { + constructor({ id, title, tip, order = 100, euiIconType, icon }: CreateManagementItemArgs) { this.id = id; this.title = title; + this.tip = tip; this.order = order; this.euiIconType = euiIconType; this.icon = icon; diff --git a/src/plugins/saved_objects_management/public/plugin.ts b/src/plugins/saved_objects_management/public/plugin.ts index f3d6318db89f2..47d445e63b942 100644 --- a/src/plugins/saved_objects_management/public/plugin.ts +++ b/src/plugins/saved_objects_management/public/plugin.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { ManagementSetup, ManagementSectionId } from '../../management/public'; +import { ManagementSetup } from '../../management/public'; import { DataPublicPluginStart } from '../../data/public'; import { DashboardStart } from '../../dashboard/public'; import { DiscoverStart } from '../../discover/public'; @@ -87,7 +87,7 @@ export class SavedObjectsManagementPlugin category: FeatureCatalogueCategory.ADMIN, }); - const kibanaSection = management.sections.getSection(ManagementSectionId.Kibana); + const kibanaSection = management.sections.section.kibana; kibanaSection.registerApp({ id: 'objects', title: i18n.translate('savedObjectsManagement.managementSectionLabel', { diff --git a/test/plugin_functional/plugins/management_test_plugin/public/plugin.tsx b/test/plugin_functional/plugins/management_test_plugin/public/plugin.tsx index 494570b26f561..9cbff335590a3 100644 --- a/test/plugin_functional/plugins/management_test_plugin/public/plugin.tsx +++ b/test/plugin_functional/plugins/management_test_plugin/public/plugin.tsx @@ -21,12 +21,12 @@ import * as React from 'react'; import ReactDOM from 'react-dom'; import { Router, Switch, Route, Link } from 'react-router-dom'; import { CoreSetup, Plugin } from 'kibana/public'; -import { ManagementSetup, ManagementSectionId } from '../../../../../src/plugins/management/public'; +import { ManagementSetup } from '../../../../../src/plugins/management/public'; export class ManagementTestPlugin implements Plugin { public setup(core: CoreSetup, { management }: { management: ManagementSetup }) { - const testSection = management.sections.getSection(ManagementSectionId.Data); + const testSection = management.sections.section.data; testSection.registerApp({ id: 'test-management', diff --git a/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts index 6c1783054a312..6008c52d0324b 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts @@ -22,7 +22,6 @@ import { import { ManagementSetup, RegisterManagementAppArgs, - ManagementSectionId, } from '../../../../../../../src/plugins/management/public'; import { LicensingPluginSetup } from '../../../../../licensing/public'; import { BeatsManagementConfigType } from '../../../../common'; @@ -105,7 +104,7 @@ export class KibanaFrameworkAdapter implements FrameworkAdapter { } public registerManagementUI(mount: RegisterManagementAppArgs['mount']) { - const section = this.management.sections.getSection(ManagementSectionId.Ingest); + const section = this.management.sections.section.ingest; section.registerApp({ id: 'beats_management', title: i18n.translate('xpack.beatsManagement.centralManagementLinkLabel', { diff --git a/x-pack/plugins/cross_cluster_replication/public/plugin.ts b/x-pack/plugins/cross_cluster_replication/public/plugin.ts index 8bf0d519e685d..7aa0d19fa976f 100644 --- a/x-pack/plugins/cross_cluster_replication/public/plugin.ts +++ b/x-pack/plugins/cross_cluster_replication/public/plugin.ts @@ -9,7 +9,6 @@ import { get } from 'lodash'; import { first } from 'rxjs/operators'; import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/public'; -import { ManagementSectionId } from '../../../../src/plugins/management/public'; import { PLUGIN, MANAGEMENT_ID } from '../common/constants'; import { init as initUiMetric } from './app/services/track_ui_metric'; import { init as initNotification } from './app/services/notifications'; @@ -23,7 +22,7 @@ export class CrossClusterReplicationPlugin implements Plugin { public setup(coreSetup: CoreSetup, plugins: PluginDependencies) { const { licensing, remoteClusters, usageCollection, management, indexManagement } = plugins; - const esSection = management.sections.getSection(ManagementSectionId.Data); + const esSection = management.sections.section.data; const { http, diff --git a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx index 49856dee47fba..832d066dfa33b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx @@ -6,7 +6,6 @@ import { CoreSetup, PluginInitializerContext } from 'src/core/public'; -import { ManagementSectionId } from '../../../../src/plugins/management/public'; import { PLUGIN } from '../common/constants'; import { init as initHttp } from './application/services/http'; import { init as initDocumentation } from './application/services/documentation'; @@ -38,7 +37,7 @@ export class IndexLifecycleManagementPlugin { initUiMetric(usageCollection); initNotification(toasts, fatalErrors); - management.sections.getSection(ManagementSectionId.Data).registerApp({ + management.sections.section.data.registerApp({ id: PLUGIN.ID, title: PLUGIN.TITLE, order: 2, diff --git a/x-pack/plugins/index_management/public/plugin.ts b/x-pack/plugins/index_management/public/plugin.ts index aec25ee3247d6..6139ed5d2e6ad 100644 --- a/x-pack/plugins/index_management/public/plugin.ts +++ b/x-pack/plugins/index_management/public/plugin.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup } from '../../../../src/core/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; -import { ManagementSetup, ManagementSectionId } from '../../../../src/plugins/management/public'; +import { ManagementSetup } from '../../../../src/plugins/management/public'; import { IngestManagerSetup } from '../../ingest_manager/public'; import { UIM_APP_NAME, PLUGIN } from '../common/constants'; @@ -51,7 +51,7 @@ export class IndexMgmtUIPlugin { notificationService.setup(notifications); this.uiMetricService.setup(usageCollection); - management.sections.getSection(ManagementSectionId.Data).registerApp({ + management.sections.section.data.registerApp({ id: PLUGIN.id, title: i18n.translate('xpack.idxMgmt.appTitle', { defaultMessage: 'Index Management' }), order: 0, diff --git a/x-pack/plugins/ingest_pipelines/public/plugin.ts b/x-pack/plugins/ingest_pipelines/public/plugin.ts index 2c1ffdd31aafe..945e825c88fbd 100644 --- a/x-pack/plugins/ingest_pipelines/public/plugin.ts +++ b/x-pack/plugins/ingest_pipelines/public/plugin.ts @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, Plugin } from 'src/core/public'; -import { ManagementSectionId } from '../../../../src/plugins/management/public'; import { PLUGIN_ID } from '../common/constants'; import { uiMetricService, apiService } from './application/services'; import { Dependencies } from './types'; @@ -21,7 +20,7 @@ export class IngestPipelinesPlugin implements Plugin { uiMetricService.setup(usageCollection); apiService.setup(http, uiMetricService); - management.sections.getSection(ManagementSectionId.Ingest).registerApp({ + management.sections.section.ingest.registerApp({ id: PLUGIN_ID, order: 1, title: i18n.translate('xpack.ingestPipelines.appTitle', { diff --git a/x-pack/plugins/license_management/public/plugin.ts b/x-pack/plugins/license_management/public/plugin.ts index 2511337793fea..b99ea387121ee 100644 --- a/x-pack/plugins/license_management/public/plugin.ts +++ b/x-pack/plugins/license_management/public/plugin.ts @@ -7,7 +7,7 @@ import { first } from 'rxjs/operators'; import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/public'; import { TelemetryPluginStart } from '../../../../src/plugins/telemetry/public'; -import { ManagementSetup, ManagementSectionId } from '../../../../src/plugins/management/public'; +import { ManagementSetup } from '../../../../src/plugins/management/public'; import { LicensingPluginSetup } from '../../../plugins/licensing/public'; import { PLUGIN } from '../common/constants'; import { ClientConfigType } from './types'; @@ -50,7 +50,7 @@ export class LicenseManagementUIPlugin const { getStartServices } = coreSetup; const { management, licensing } = plugins; - management.sections.getSection(ManagementSectionId.Stack).registerApp({ + management.sections.section.stack.registerApp({ id: PLUGIN.id, title: PLUGIN.title, order: 0, diff --git a/x-pack/plugins/logstash/public/plugin.ts b/x-pack/plugins/logstash/public/plugin.ts index ade6abdb63f43..59f92ee0a7ffc 100644 --- a/x-pack/plugins/logstash/public/plugin.ts +++ b/x-pack/plugins/logstash/public/plugin.ts @@ -14,7 +14,7 @@ import { HomePublicPluginSetup, FeatureCatalogueCategory, } from '../../../../src/plugins/home/public'; -import { ManagementSetup, ManagementSectionId } from '../../../../src/plugins/management/public'; +import { ManagementSetup } from '../../../../src/plugins/management/public'; import { LicensingPluginSetup } from '../../licensing/public'; // @ts-ignore @@ -35,22 +35,20 @@ export class LogstashPlugin implements Plugin { map((license) => new LogstashLicenseService(license)) ); - const managementApp = plugins.management.sections - .getSection(ManagementSectionId.Ingest) - .registerApp({ - id: 'pipelines', - title: i18n.translate('xpack.logstash.managementSection.pipelinesTitle', { - defaultMessage: 'Logstash Pipelines', - }), - order: 1, - mount: async (params) => { - const [coreStart] = await core.getStartServices(); - const { renderApp } = await import('./application'); - const isMonitoringEnabled = 'monitoring' in plugins; + const managementApp = plugins.management.sections.section.ingest.registerApp({ + id: 'pipelines', + title: i18n.translate('xpack.logstash.managementSection.pipelinesTitle', { + defaultMessage: 'Logstash Pipelines', + }), + order: 1, + mount: async (params) => { + const [coreStart] = await core.getStartServices(); + const { renderApp } = await import('./application'); + const isMonitoringEnabled = 'monitoring' in plugins; - return renderApp(coreStart, params, isMonitoringEnabled, logstashLicense$); - }, - }); + return renderApp(coreStart, params, isMonitoringEnabled, logstashLicense$); + }, + }); this.licenseSubscription = logstashLicense$.subscribe((license: any) => { if (license.enableLinks) { diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index a08b9b6d97116..c61db9fb1ad8d 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -30,7 +30,6 @@ "esUiShared", "kibanaUtils", "kibanaReact", - "management", "dashboard", "savedObjects" ] diff --git a/x-pack/plugins/ml/public/application/management/index.ts b/x-pack/plugins/ml/public/application/management/index.ts index 480e2fe488980..897731304ee7a 100644 --- a/x-pack/plugins/ml/public/application/management/index.ts +++ b/x-pack/plugins/ml/public/application/management/index.ts @@ -16,10 +16,7 @@ import { take } from 'rxjs/operators'; import { CoreSetup } from 'kibana/public'; import { MlStartDependencies, MlSetupDependencies } from '../../plugin'; -import { - ManagementAppMountParams, - ManagementSectionId, -} from '../../../../../../src/plugins/management/public'; +import { ManagementAppMountParams } from '../../../../../../src/plugins/management/public'; import { PLUGIN_ID } from '../../../common/constants/app'; import { MINIMUM_FULL_LICENSE } from '../../../common/license'; @@ -34,7 +31,7 @@ export function initManagementSection( management !== undefined && license.check(PLUGIN_ID, MINIMUM_FULL_LICENSE).state === 'valid' ) { - management.sections.getSection(ManagementSectionId.InsightsAndAlerting).registerApp({ + management.sections.section.insightsAndAlerting.registerApp({ id: 'jobsListLink', title: i18n.translate('xpack.ml.management.jobsListTitle', { defaultMessage: 'Machine Learning Jobs', diff --git a/x-pack/plugins/remote_clusters/public/plugin.ts b/x-pack/plugins/remote_clusters/public/plugin.ts index 8881db0f9196e..33222dd7052e9 100644 --- a/x-pack/plugins/remote_clusters/public/plugin.ts +++ b/x-pack/plugins/remote_clusters/public/plugin.ts @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, Plugin, CoreStart, PluginInitializerContext } from 'kibana/public'; -import { ManagementSectionId } from '../../../../src/plugins/management/public'; import { init as initBreadcrumbs } from './application/services/breadcrumb'; import { init as initDocumentation } from './application/services/documentation'; import { init as initHttp } from './application/services/http'; @@ -33,7 +32,7 @@ export class RemoteClustersUIPlugin } = this.initializerContext.config.get(); if (isRemoteClustersUiEnabled) { - const esSection = management.sections.getSection(ManagementSectionId.Data); + const esSection = management.sections.section.data; esSection.registerApp({ id: 'remote_clusters', diff --git a/x-pack/plugins/reporting/public/plugin.tsx b/x-pack/plugins/reporting/public/plugin.tsx index 8a25df0a74bbf..d003d4c581699 100644 --- a/x-pack/plugins/reporting/public/plugin.tsx +++ b/x-pack/plugins/reporting/public/plugin.tsx @@ -23,7 +23,7 @@ import { FeatureCatalogueCategory, HomePublicPluginSetup, } from '../../../../src/plugins/home/public'; -import { ManagementSectionId, ManagementSetup } from '../../../../src/plugins/management/public'; +import { ManagementSetup } from '../../../../src/plugins/management/public'; import { SharePluginSetup } from '../../../../src/plugins/share/public'; import { LicensingPluginSetup } from '../../licensing/public'; import { JobId, JobStatusBuckets, ReportingConfigType } from '../common/types'; @@ -115,8 +115,7 @@ export class ReportingPublicPlugin implements Plugin { showOnHomePage: false, category: FeatureCatalogueCategory.ADMIN, }); - - management.sections.getSection(ManagementSectionId.InsightsAndAlerting).registerApp({ + management.sections.section.insightsAndAlerting.registerApp({ id: 'reporting', title: this.title, order: 1, diff --git a/x-pack/plugins/rollup/public/plugin.ts b/x-pack/plugins/rollup/public/plugin.ts index b55760c5cc5aa..73ee675b089c8 100644 --- a/x-pack/plugins/rollup/public/plugin.ts +++ b/x-pack/plugins/rollup/public/plugin.ts @@ -16,7 +16,7 @@ import { FeatureCatalogueCategory, HomePublicPluginSetup, } from '../../../../src/plugins/home/public'; -import { ManagementSetup, ManagementSectionId } from '../../../../src/plugins/management/public'; +import { ManagementSetup } from '../../../../src/plugins/management/public'; import { IndexManagementPluginSetup } from '../../index_management/public'; import { IndexPatternManagementSetup } from '../../../../src/plugins/index_pattern_management/public'; // @ts-ignore @@ -75,7 +75,7 @@ export class RollupPlugin implements Plugin { }); } - management.sections.getSection(ManagementSectionId.Data).registerApp({ + management.sections.section.data.registerApp({ id: 'rollup_jobs', title: i18n.translate('xpack.rollupJobs.appTitle', { defaultMessage: 'Rollup Jobs' }), order: 4, diff --git a/x-pack/plugins/security/kibana.json b/x-pack/plugins/security/kibana.json index 0daab9d5dbce3..064ff5b6a6711 100644 --- a/x-pack/plugins/security/kibana.json +++ b/x-pack/plugins/security/kibana.json @@ -9,10 +9,8 @@ "ui": true, "requiredBundles": [ "home", - "management", "kibanaReact", "spaces", - "esUiShared", - "management" + "esUiShared" ] } diff --git a/x-pack/plugins/security/public/management/management_service.test.ts b/x-pack/plugins/security/public/management/management_service.test.ts index c707206569bf5..ce93fb7c98f41 100644 --- a/x-pack/plugins/security/public/management/management_service.test.ts +++ b/x-pack/plugins/security/public/management/management_service.test.ts @@ -8,8 +8,9 @@ import { BehaviorSubject } from 'rxjs'; import { ManagementApp, ManagementSetup, - ManagementStart, + DefinedSections, } from '../../../../../src/plugins/management/public'; +import { createManagementSectionMock } from '../../../../../src/plugins/management/public/mocks'; import { SecurityLicenseFeatures } from '../../common/licensing/license_features'; import { ManagementService } from './management_service'; import { usersManagementApp } from './users'; @@ -21,7 +22,7 @@ import { rolesManagementApp } from './roles'; import { apiKeysManagementApp } from './api_keys'; import { roleMappingsManagementApp } from './role_mappings'; -const mockSection = { registerApp: jest.fn() }; +const mockSection = createManagementSectionMock(); describe('ManagementService', () => { describe('setup()', () => { @@ -32,8 +33,10 @@ describe('ManagementService', () => { const managementSetup: ManagementSetup = { sections: { - register: jest.fn(), - getSection: jest.fn().mockReturnValue(mockSection), + register: jest.fn(() => mockSection), + section: { + security: mockSection, + } as DefinedSections, }, }; @@ -88,8 +91,10 @@ describe('ManagementService', () => { const managementSetup: ManagementSetup = { sections: { - register: jest.fn(), - getSection: jest.fn().mockReturnValue(mockSection), + register: jest.fn(() => mockSection), + section: { + security: mockSection, + } as DefinedSections, }, }; @@ -116,6 +121,7 @@ describe('ManagementService', () => { }), } as unknown) as jest.Mocked; }; + mockSection.getApp = jest.fn().mockImplementation((id) => mockApps.get(id)); const mockApps = new Map>([ [usersManagementApp.id, getMockedApp()], [rolesManagementApp.id, getMockedApp()], @@ -123,19 +129,7 @@ describe('ManagementService', () => { [roleMappingsManagementApp.id, getMockedApp()], ] as Array<[string, jest.Mocked]>); - const managementStart: ManagementStart = { - sections: { - getSection: jest - .fn() - .mockReturnValue({ getApp: jest.fn().mockImplementation((id) => mockApps.get(id)) }), - getAllSections: jest.fn(), - getSectionsEnabled: jest.fn(), - }, - }; - - service.start({ - management: managementStart, - }); + service.start(); return { mockApps, diff --git a/x-pack/plugins/security/public/management/management_service.ts b/x-pack/plugins/security/public/management/management_service.ts index 148d2855ba9b7..199fd917da071 100644 --- a/x-pack/plugins/security/public/management/management_service.ts +++ b/x-pack/plugins/security/public/management/management_service.ts @@ -9,8 +9,7 @@ import { StartServicesAccessor, FatalErrorsSetup } from 'src/core/public'; import { ManagementApp, ManagementSetup, - ManagementStart, - ManagementSectionId, + ManagementSection, } from '../../../../../src/plugins/management/public'; import { SecurityLicense } from '../../common/licensing'; import { AuthenticationServiceSetup } from '../authentication'; @@ -28,30 +27,26 @@ interface SetupParams { getStartServices: StartServicesAccessor; } -interface StartParams { - management: ManagementStart; -} - export class ManagementService { private license!: SecurityLicense; private licenseFeaturesSubscription?: Subscription; + private securitySection?: ManagementSection; setup({ getStartServices, management, authc, license, fatalErrors }: SetupParams) { this.license = license; + this.securitySection = management.sections.section.security; - const securitySection = management.sections.getSection(ManagementSectionId.Security); - - securitySection.registerApp(usersManagementApp.create({ authc, getStartServices })); - securitySection.registerApp( + this.securitySection.registerApp(usersManagementApp.create({ authc, getStartServices })); + this.securitySection.registerApp( rolesManagementApp.create({ fatalErrors, license, getStartServices }) ); - securitySection.registerApp(apiKeysManagementApp.create({ getStartServices })); - securitySection.registerApp(roleMappingsManagementApp.create({ getStartServices })); + this.securitySection.registerApp(apiKeysManagementApp.create({ getStartServices })); + this.securitySection.registerApp(roleMappingsManagementApp.create({ getStartServices })); } - start({ management }: StartParams) { + start() { this.licenseFeaturesSubscription = this.license.features$.subscribe(async (features) => { - const securitySection = management.sections.getSection(ManagementSectionId.Security); + const securitySection = this.securitySection!; const securityManagementAppsStatuses: Array<[ManagementApp, boolean]> = [ [securitySection.getApp(usersManagementApp.id)!, features.showLinks], diff --git a/x-pack/plugins/security/public/plugin.test.tsx b/x-pack/plugins/security/public/plugin.test.tsx index 7c57c4dd997a2..8cec4fbc2f5a2 100644 --- a/x-pack/plugins/security/public/plugin.test.tsx +++ b/x-pack/plugins/security/public/plugin.test.tsx @@ -33,7 +33,9 @@ describe('Security Plugin', () => { coreMock.createSetup({ basePath: '/some-base-path' }) as CoreSetup< PluginStartDependencies >, - { licensing: licensingMock.createSetup() } + { + licensing: licensingMock.createSetup(), + } ) ).toEqual({ __legacyCompat: { logoutUrl: '/some-base-path/logout', tenant: '/some-base-path' }, @@ -117,7 +119,6 @@ describe('Security Plugin', () => { }); expect(startManagementServiceMock).toHaveBeenCalledTimes(1); - expect(startManagementServiceMock).toHaveBeenCalledWith({ management: managementStartMock }); }); }); diff --git a/x-pack/plugins/security/public/plugin.tsx b/x-pack/plugins/security/public/plugin.tsx index da69dd051c11d..bef183bd97e8c 100644 --- a/x-pack/plugins/security/public/plugin.tsx +++ b/x-pack/plugins/security/public/plugin.tsx @@ -139,9 +139,8 @@ export class SecurityPlugin public start(core: CoreStart, { management }: PluginStartDependencies) { this.sessionTimeout.start(); this.navControlService.start({ core }); - if (management) { - this.managementService.start({ management }); + this.managementService.start(); } } diff --git a/x-pack/plugins/snapshot_restore/public/plugin.ts b/x-pack/plugins/snapshot_restore/public/plugin.ts index ee27886948a54..b864e70708652 100644 --- a/x-pack/plugins/snapshot_restore/public/plugin.ts +++ b/x-pack/plugins/snapshot_restore/public/plugin.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, PluginInitializerContext } from 'src/core/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; -import { ManagementSetup, ManagementSectionId } from '../../../../src/plugins/management/public'; +import { ManagementSetup } from '../../../../src/plugins/management/public'; import { PLUGIN } from '../common/constants'; import { ClientConfigType } from './types'; @@ -40,7 +40,7 @@ export class SnapshotRestoreUIPlugin { textService.setup(i18n); httpService.setup(http); - management.sections.getSection(ManagementSectionId.Data).registerApp({ + management.sections.section.data.registerApp({ id: PLUGIN.id, title: i18n.translate('xpack.snapshotRestore.appTitle', { defaultMessage: 'Snapshot and Restore', diff --git a/x-pack/plugins/spaces/kibana.json b/x-pack/plugins/spaces/kibana.json index 0698535cc15fd..4443b6d8a685b 100644 --- a/x-pack/plugins/spaces/kibana.json +++ b/x-pack/plugins/spaces/kibana.json @@ -18,7 +18,6 @@ "requiredBundles": [ "kibanaReact", "savedObjectsManagement", - "management", "home" ] } diff --git a/x-pack/plugins/spaces/public/management/management_service.test.ts b/x-pack/plugins/spaces/public/management/management_service.test.ts index eb543d44ecb4b..444ccf43d3d1f 100644 --- a/x-pack/plugins/spaces/public/management/management_service.test.ts +++ b/x-pack/plugins/spaces/public/management/management_service.test.ts @@ -18,22 +18,19 @@ describe('ManagementService', () => { const mockKibanaSection = ({ registerApp: jest.fn(), } as unknown) as ManagementSection; + const managementMockSetup = managementPluginMock.createSetupContract(); + managementMockSetup.sections.section.kibana = mockKibanaSection; const deps = { - management: managementPluginMock.createSetupContract(), + management: managementMockSetup, getStartServices: coreMock.createSetup().getStartServices as CoreSetup< PluginsStart >['getStartServices'], spacesManager: spacesManagerMock.create(), }; - deps.management.sections.getSection.mockReturnValue(mockKibanaSection); - const service = new ManagementService(); service.setup(deps); - expect(deps.management.sections.getSection).toHaveBeenCalledTimes(1); - expect(deps.management.sections.getSection).toHaveBeenCalledWith('kibana'); - expect(mockKibanaSection.registerApp).toHaveBeenCalledTimes(1); expect(mockKibanaSection.registerApp).toHaveBeenCalledWith({ id: 'spaces', @@ -63,20 +60,17 @@ describe('ManagementService', () => { const mockKibanaSection = ({ registerApp: jest.fn().mockReturnValue(mockSpacesManagementPage), } as unknown) as ManagementSection; + const managementMockSetup = managementPluginMock.createSetupContract(); + managementMockSetup.sections.section.kibana = mockKibanaSection; const deps = { - management: managementPluginMock.createSetupContract(), + management: managementMockSetup, getStartServices: coreMock.createSetup().getStartServices as CoreSetup< PluginsStart >['getStartServices'], spacesManager: spacesManagerMock.create(), }; - deps.management.sections.getSection.mockImplementation((id) => { - if (id === 'kibana') return mockKibanaSection; - throw new Error(`unexpected getSection call: ${id}`); - }); - const service = new ManagementService(); service.setup(deps); diff --git a/x-pack/plugins/spaces/public/management/management_service.tsx b/x-pack/plugins/spaces/public/management/management_service.tsx index 4d5a1b32b31a3..11853e5f1abdd 100644 --- a/x-pack/plugins/spaces/public/management/management_service.tsx +++ b/x-pack/plugins/spaces/public/management/management_service.tsx @@ -5,11 +5,7 @@ */ import { StartServicesAccessor } from 'src/core/public'; -import { - ManagementSetup, - ManagementApp, - ManagementSectionId, -} from '../../../../../src/plugins/management/public'; +import { ManagementSetup, ManagementApp } from '../../../../../src/plugins/management/public'; import { SecurityLicense } from '../../../security/public'; import { SpacesManager } from '../spaces_manager'; import { PluginsStart } from '../plugin'; @@ -26,11 +22,9 @@ export class ManagementService { private registeredSpacesManagementApp?: ManagementApp; public setup({ getStartServices, management, spacesManager, securityLicense }: SetupDeps) { - this.registeredSpacesManagementApp = management.sections - .getSection(ManagementSectionId.Kibana) - .registerApp( - spacesManagementApp.create({ getStartServices, spacesManager, securityLicense }) - ); + this.registeredSpacesManagementApp = management.sections.section.kibana.registerApp( + spacesManagementApp.create({ getStartServices, spacesManager, securityLicense }) + ); } public stop() { diff --git a/x-pack/plugins/spaces/public/plugin.test.ts b/x-pack/plugins/spaces/public/plugin.test.ts index 4a49cf20d3a4a..d8eecb9c7e606 100644 --- a/x-pack/plugins/spaces/public/plugin.test.ts +++ b/x-pack/plugins/spaces/public/plugin.test.ts @@ -7,8 +7,10 @@ import { coreMock } from 'src/core/public/mocks'; import { SpacesPlugin } from './plugin'; import { homePluginMock } from '../../../../src/plugins/home/public/mocks'; -import { ManagementSection, ManagementSectionId } from '../../../../src/plugins/management/public'; -import { managementPluginMock } from '../../../../src/plugins/management/public/mocks'; +import { + managementPluginMock, + createManagementSectionMock, +} from '../../../../src/plugins/management/public/mocks'; import { advancedSettingsMock } from '../../../../src/plugins/advanced_settings/public/mocks'; import { featuresPluginMock } from '../../features/public/mocks'; @@ -32,19 +34,13 @@ describe('Spaces plugin', () => { it('should register the management and feature catalogue sections when the management and home plugins are both available', () => { const coreSetup = coreMock.createSetup(); - - const kibanaSection = new ManagementSection({ - id: ManagementSectionId.Kibana, - title: 'Mock Kibana Section', - order: 1, - }); - - const registerAppSpy = jest.spyOn(kibanaSection, 'registerApp'); - const home = homePluginMock.createSetupContract(); const management = managementPluginMock.createSetupContract(); - management.sections.getSection.mockReturnValue(kibanaSection); + const mockSection = createManagementSectionMock(); + mockSection.registerApp = jest.fn(); + + management.sections.section.kibana = mockSection; const plugin = new SpacesPlugin(); plugin.setup(coreSetup, { @@ -52,7 +48,9 @@ describe('Spaces plugin', () => { home, }); - expect(registerAppSpy).toHaveBeenCalledWith(expect.objectContaining({ id: 'spaces' })); + expect(mockSection.registerApp).toHaveBeenCalledWith( + expect.objectContaining({ id: 'spaces' }) + ); expect(home.featureCatalogue.register).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/x-pack/plugins/transform/public/plugin.ts b/x-pack/plugins/transform/public/plugin.ts index 27d1ad29f51d0..74256a478e732 100644 --- a/x-pack/plugins/transform/public/plugin.ts +++ b/x-pack/plugins/transform/public/plugin.ts @@ -8,7 +8,7 @@ import { i18n as kbnI18n } from '@kbn/i18n'; import { CoreSetup } from 'src/core/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { HomePublicPluginSetup } from 'src/plugins/home/public'; -import { ManagementSetup, ManagementSectionId } from '../../../../src/plugins/management/public'; +import { ManagementSetup } from '../../../../src/plugins/management/public'; import { registerFeature } from './register_feature'; export interface PluginsDependencies { @@ -22,7 +22,7 @@ export class TransformUiPlugin { const { management, home } = pluginsSetup; // Register management section - const esSection = management.sections.getSection(ManagementSectionId.Data); + const esSection = management.sections.section.data; esSection.registerApp({ id: 'transform', title: kbnI18n.translate('xpack.transform.appTitle', { diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index db93e48ab3692..af4d2784cfa67 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreStart, PluginInitializerContext, Plugin as CorePlugin } from 'src/core/public'; +import { + CoreStart, + CoreSetup, + PluginInitializerContext, + Plugin as CorePlugin, +} from 'src/core/public'; import { i18n } from '@kbn/i18n'; import { registerBuiltInActionTypes } from './application/components/builtin_action_types'; @@ -12,7 +17,11 @@ import { registerBuiltInAlertTypes } from './application/components/builtin_aler import { hasShowActionsCapability, hasShowAlertsCapability } from './application/lib/capabilities'; import { ActionTypeModel, AlertTypeModel } from './types'; import { TypeRegistry } from './application/type_registry'; -import { ManagementStart, ManagementSectionId } from '../../../../src/plugins/management/public'; +import { + ManagementSetup, + ManagementAppMountParams, + ManagementApp, +} from '../../../../src/plugins/management/public'; import { boot } from './application/boot'; import { ChartsPluginStart } from '../../../../src/plugins/charts/public'; import { PluginStartContract as AlertingStart } from '../../alerts/public'; @@ -28,10 +37,13 @@ export interface TriggersAndActionsUIPublicPluginStart { alertTypeRegistry: TypeRegistry; } +interface PluginsSetup { + management: ManagementSetup; +} + interface PluginsStart { data: DataPublicPluginStart; charts: ChartsPluginStart; - management: ManagementStart; alerts?: AlertingStart; navigateToApp: CoreStart['application']['navigateToApp']; } @@ -41,6 +53,7 @@ export class Plugin CorePlugin { private actionTypeRegistry: TypeRegistry; private alertTypeRegistry: TypeRegistry; + private managementApp?: ManagementApp; constructor(initializerContext: PluginInitializerContext) { const actionTypeRegistry = new TypeRegistry(); @@ -50,7 +63,45 @@ export class Plugin this.alertTypeRegistry = alertTypeRegistry; } - public setup(): TriggersAndActionsUIPublicPluginSetup { + public setup(core: CoreSetup, plugins: PluginsSetup): TriggersAndActionsUIPublicPluginSetup { + const actionTypeRegistry = this.actionTypeRegistry; + const alertTypeRegistry = this.alertTypeRegistry; + + this.managementApp = plugins.management.sections.section.insightsAndAlerting.registerApp({ + id: 'triggersActions', + title: i18n.translate('xpack.triggersActionsUI.managementSection.displayName', { + defaultMessage: 'Alerts and Actions', + }), + order: 0, + async mount(params: ManagementAppMountParams) { + const [coreStart, pluginsStart] = (await core.getStartServices()) as [ + CoreStart, + PluginsStart, + unknown + ]; + boot({ + dataPlugin: pluginsStart.data, + charts: pluginsStart.charts, + alerts: pluginsStart.alerts, + element: params.element, + toastNotifications: coreStart.notifications.toasts, + http: coreStart.http, + uiSettings: coreStart.uiSettings, + docLinks: coreStart.docLinks, + chrome: coreStart.chrome, + savedObjects: coreStart.savedObjects.client, + I18nContext: coreStart.i18n.Context, + capabilities: coreStart.application.capabilities, + navigateToApp: coreStart.application.navigateToApp, + setBreadcrumbs: params.setBreadcrumbs, + history: params.history, + actionTypeRegistry, + alertTypeRegistry, + }); + return () => {}; + }, + }); + registerBuiltInActionTypes({ actionTypeRegistry: this.actionTypeRegistry, }); @@ -65,43 +116,18 @@ export class Plugin }; } - public start(core: CoreStart, plugins: PluginsStart): TriggersAndActionsUIPublicPluginStart { + public start(core: CoreStart): TriggersAndActionsUIPublicPluginStart { const { capabilities } = core.application; const canShowActions = hasShowActionsCapability(capabilities); const canShowAlerts = hasShowAlertsCapability(capabilities); + const managementApp = this.managementApp as ManagementApp; // Don't register routes when user doesn't have access to the application if (canShowActions || canShowAlerts) { - plugins.management.sections.getSection(ManagementSectionId.InsightsAndAlerting).registerApp({ - id: 'triggersActions', - title: i18n.translate('xpack.triggersActionsUI.managementSection.displayName', { - defaultMessage: 'Alerts and Actions', - }), - order: 0, - mount: (params) => { - boot({ - dataPlugin: plugins.data, - charts: plugins.charts, - alerts: plugins.alerts, - element: params.element, - toastNotifications: core.notifications.toasts, - http: core.http, - uiSettings: core.uiSettings, - docLinks: core.docLinks, - chrome: core.chrome, - savedObjects: core.savedObjects.client, - I18nContext: core.i18n.Context, - capabilities: core.application.capabilities, - navigateToApp: core.application.navigateToApp, - setBreadcrumbs: params.setBreadcrumbs, - history: params.history, - actionTypeRegistry: this.actionTypeRegistry, - alertTypeRegistry: this.alertTypeRegistry, - }); - return () => {}; - }, - }); + managementApp.enable(); + } else { + managementApp.disable(); } return { actionTypeRegistry: this.actionTypeRegistry, diff --git a/x-pack/plugins/upgrade_assistant/kibana.json b/x-pack/plugins/upgrade_assistant/kibana.json index 31109dd963ab4..273036a653aeb 100644 --- a/x-pack/plugins/upgrade_assistant/kibana.json +++ b/x-pack/plugins/upgrade_assistant/kibana.json @@ -5,6 +5,5 @@ "ui": true, "configPath": ["xpack", "upgrade_assistant"], "requiredPlugins": ["management", "licensing"], - "optionalPlugins": ["cloud", "usageCollection"], - "requiredBundles": ["management"] + "optionalPlugins": ["cloud", "usageCollection"] } diff --git a/x-pack/plugins/upgrade_assistant/public/plugin.ts b/x-pack/plugins/upgrade_assistant/public/plugin.ts index be00a030d5a27..01c1a6a4659d5 100644 --- a/x-pack/plugins/upgrade_assistant/public/plugin.ts +++ b/x-pack/plugins/upgrade_assistant/public/plugin.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { Plugin, CoreSetup, PluginInitializerContext } from 'src/core/public'; import { CloudSetup } from '../../cloud/public'; -import { ManagementSetup, ManagementSectionId } from '../../../../src/plugins/management/public'; +import { ManagementSetup } from '../../../../src/plugins/management/public'; import { NEXT_MAJOR_VERSION } from '../common/version'; import { Config } from '../common/config'; @@ -24,7 +24,7 @@ export class UpgradeAssistantUIPlugin implements Plugin { if (!enabled) { return; } - const appRegistrar = management.sections.getSection(ManagementSectionId.Stack); + const appRegistrar = management.sections.section.stack; const isCloudEnabled = Boolean(cloud?.isCloudEnabled); appRegistrar.registerApp({ diff --git a/x-pack/plugins/watcher/public/plugin.ts b/x-pack/plugins/watcher/public/plugin.ts index 3d907ac0dff3a..6b66c341497b7 100644 --- a/x-pack/plugins/watcher/public/plugin.ts +++ b/x-pack/plugins/watcher/public/plugin.ts @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, Plugin, CoreStart } from 'kibana/public'; import { first, map, skip } from 'rxjs/operators'; -import { ManagementSectionId } from '../../../../src/plugins/management/public'; import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; import { LicenseStatus } from '../common/types/license_status'; @@ -29,7 +28,7 @@ export class WatcherUIPlugin implements Plugin { { notifications, http, uiSettings, getStartServices }: CoreSetup, { licensing, management, data, home, charts }: Dependencies ) { - const esSection = management.sections.getSection(ManagementSectionId.InsightsAndAlerting); + const esSection = management.sections.section.insightsAndAlerting; const watcherESApp = esSection.registerApp({ id: 'watcher', From 52bbffff6120ad30a8eab1061faa1b0553613994 Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Tue, 14 Jul 2020 18:32:07 +0200 Subject: [PATCH 14/82] [Security][Detections] Create Threshold-based Rule type (#71371) --- .../schemas/common/schemas.ts | 18 +- .../request/add_prepackaged_rules_schema.ts | 2 + ..._prepackaged_rules_type_dependents.test.ts | 24 ++- .../add_prepackaged_rules_type_dependents.ts | 14 ++ .../schemas/request/create_rules_schema.ts | 2 + .../create_rules_type_dependents.test.ts | 22 ++ .../request/create_rules_type_dependents.ts | 14 ++ .../schemas/request/import_rules_schema.ts | 2 + .../import_rules_type_dependents.test.ts | 22 ++ .../request/import_rules_type_dependents.ts | 14 ++ .../schemas/request/patch_rules_schema.ts | 2 + ...ts => patch_rules_type_dependents.test.ts} | 22 ++ .../request/patch_rules_type_dependents.ts | 14 ++ .../schemas/request/update_rules_schema.ts | 2 + .../update_rules_type_dependents.test.ts | 22 ++ .../request/update_rules_type_dependents.ts | 14 ++ .../schemas/response/rules_schema.ts | 18 +- .../common/detection_engine/types.ts | 1 + .../components/alerts_table/actions.test.tsx | 7 + .../components/alerts_table/actions.tsx | 101 ++++++++- .../alerts_table/default_config.tsx | 3 +- .../components/alerts_table/types.ts | 3 +- .../rules/description_step/helpers.test.tsx | 12 ++ .../rules/description_step/helpers.tsx | 26 ++- .../rules/description_step/index.test.tsx | 48 ++++- .../rules/description_step/index.tsx | 4 + .../rules/description_step/translations.tsx | 21 ++ .../rules/select_rule_type/index.tsx | 98 +++++---- .../select_rule_type/ml_card_description.tsx | 48 +++++ .../rules/select_rule_type/translations.ts | 14 ++ .../rules/step_define_rule/index.tsx | 70 ++++-- .../rules/step_define_rule/schema.tsx | 32 +++ .../rules/threshold_input/index.tsx | 80 +++++++ .../rules/threshold_input/translations.ts | 14 ++ .../detection_engine/rules/types.ts | 3 + .../rules/all/__mocks__/mock.ts | 8 + .../detection_engine/rules/create/helpers.ts | 32 ++- .../detection_engine/rules/helpers.test.tsx | 12 ++ .../pages/detection_engine/rules/helpers.tsx | 22 +- .../pages/detection_engine/rules/types.ts | 6 + .../public/graphql/introspection.json | 8 + .../security_solution/public/graphql/types.ts | 6 + .../public/shared_imports.ts | 1 + .../timeline/body/actions/index.tsx | 8 +- .../body/events/event_column_view.tsx | 32 +-- .../timelines/containers/index.gql_query.ts | 2 + .../server/graphql/ecs/schema.gql.ts | 1 + .../security_solution/server/graphql/types.ts | 9 + .../routes/__mocks__/request_responses.ts | 1 + .../routes/index/signals_mapping.json | 13 ++ .../routes/rules/create_rules_bulk_route.ts | 2 + .../routes/rules/create_rules_route.ts | 4 + .../routes/rules/import_rules_route.ts | 3 + .../routes/rules/patch_rules_bulk_route.ts | 2 + .../routes/rules/patch_rules_route.ts | 2 + .../routes/rules/update_rules_bulk_route.ts | 2 + .../routes/rules/update_rules_route.ts | 2 + .../detection_engine/routes/rules/utils.ts | 1 + .../rules/create_rules.mock.ts | 2 + .../detection_engine/rules/create_rules.ts | 2 + .../rules/install_prepacked_rules.ts | 2 + .../rules/patch_rules.mock.ts | 2 + .../lib/detection_engine/rules/patch_rules.ts | 3 + .../lib/detection_engine/rules/types.ts | 4 + .../rules/update_prepacked_rules.ts | 2 + .../rules/update_rules.mock.ts | 2 + .../detection_engine/rules/update_rules.ts | 3 + .../lib/detection_engine/rules/utils.test.ts | 3 + .../lib/detection_engine/rules/utils.ts | 2 + .../signals/__mocks__/es_results.ts | 1 + .../signals/build_events_query.test.ts | 154 ++++++++++++++ .../signals/build_events_query.ts | 4 + .../detection_engine/signals/build_rule.ts | 1 + .../detection_engine/signals/build_signal.ts | 8 +- .../bulk_create_threshold_signals.test.ts | 196 +++++++++++++++++ .../signals/bulk_create_threshold_signals.ts | 199 ++++++++++++++++++ .../signals/find_threshold_signals.ts | 61 ++++++ .../detection_engine/signals/get_filter.ts | 81 ++++--- .../signals/signal_params_schema.ts | 3 + .../signals/signal_rule_alert_type.ts | 58 +++++ .../signals/single_search_after.ts | 3 + .../lib/detection_engine/signals/types.ts | 1 + .../server/lib/detection_engine/types.ts | 2 + .../server/lib/ecs_fields/index.ts | 1 + 84 files changed, 1656 insertions(+), 136 deletions(-) rename x-pack/plugins/security_solution/common/detection_engine/schemas/request/{patch_rule_type_dependents.test.ts => patch_rules_type_dependents.test.ts} (79%) create mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/ml_card_description.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/threshold_input/index.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/threshold_input/translations.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index 74c127365ddee..542cbe8916032 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -275,7 +275,12 @@ export type To = t.TypeOf; export const toOrUndefined = t.union([to, t.undefined]); export type ToOrUndefined = t.TypeOf; -export const type = t.keyof({ machine_learning: null, query: null, saved_query: null }); +export const type = t.keyof({ + machine_learning: null, + query: null, + saved_query: null, + threshold: null, +}); export type Type = t.TypeOf; export const typeOrUndefined = t.union([type, t.undefined]); @@ -369,6 +374,17 @@ export type Threat = t.TypeOf; export const threatOrUndefined = t.union([threat, t.undefined]); export type ThreatOrUndefined = t.TypeOf; +export const threshold = t.exact( + t.type({ + field: t.string, + value: PositiveIntegerGreaterThanZero, + }) +); +export type Threshold = t.TypeOf; + +export const thresholdOrUndefined = t.union([threshold, t.undefined]); +export type ThresholdOrUndefined = t.TypeOf; + export const created_at = IsoDateString; export const updated_at = IsoDateString; export const updated_by = t.string; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts index bf96be5e688fa..aebc3361f6e49 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts @@ -25,6 +25,7 @@ import { To, type, Threat, + threshold, ThrottleOrNull, note, References, @@ -111,6 +112,7 @@ export const addPrepackagedRulesSchema = t.intersection([ tags: DefaultStringArray, // defaults to empty string array if not set during decode to: DefaultToString, // defaults to "now" if not set during decode threat: DefaultThreatArray, // defaults to empty array if not set during decode + threshold, // defaults to "undefined" if not set during decode throttle: DefaultThrottleNull, // defaults to "null" if not set during decode timestamp_override, // defaults to "undefined" if not set during decode references: DefaultStringArray, // defaults to empty array of strings if not set during decode diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_type_dependents.test.ts index 793d4b04ed0e5..f844d0e86e1f9 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_type_dependents.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_type_dependents.test.ts @@ -8,7 +8,7 @@ import { AddPrepackagedRulesSchema } from './add_prepackaged_rules_schema'; import { addPrepackagedRuleValidateTypeDependents } from './add_prepackaged_rules_type_dependents'; import { getAddPrepackagedRulesSchemaMock } from './add_prepackaged_rules_schema.mock'; -describe('create_rules_type_dependents', () => { +describe('add_prepackaged_rules_type_dependents', () => { test('saved_id is required when type is saved_query and will not validate without out', () => { const schema: AddPrepackagedRulesSchema = { ...getAddPrepackagedRulesSchemaMock(), @@ -68,4 +68,26 @@ describe('create_rules_type_dependents', () => { const errors = addPrepackagedRuleValidateTypeDependents(schema); expect(errors).toEqual(['when "timeline_title" exists, "timeline_id" must also exist']); }); + + test('threshold is required when type is threshold and validates with it', () => { + const schema: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + type: 'threshold', + }; + const errors = addPrepackagedRuleValidateTypeDependents(schema); + expect(errors).toEqual(['when "type" is "threshold", "threshold" is required']); + }); + + test('threshold.value is required and has to be bigger than 0 when type is threshold and validates with it', () => { + const schema: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + type: 'threshold', + threshold: { + field: '', + value: -1, + }, + }; + const errors = addPrepackagedRuleValidateTypeDependents(schema); + expect(errors).toEqual(['"threshold.value" has to be bigger than 0']); + }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_type_dependents.ts index 2788c331154d2..6a51f724fc9e6 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_type_dependents.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_type_dependents.ts @@ -92,6 +92,19 @@ export const validateTimelineTitle = (rule: AddPrepackagedRulesSchema): string[] return []; }; +export const validateThreshold = (rule: AddPrepackagedRulesSchema): string[] => { + if (rule.type === 'threshold') { + if (!rule.threshold) { + return ['when "type" is "threshold", "threshold" is required']; + } else if (rule.threshold.value <= 0) { + return ['"threshold.value" has to be bigger than 0']; + } else { + return []; + } + } + return []; +}; + export const addPrepackagedRuleValidateTypeDependents = ( schema: AddPrepackagedRulesSchema ): string[] => { @@ -103,5 +116,6 @@ export const addPrepackagedRuleValidateTypeDependents = ( ...validateMachineLearningJobId(schema), ...validateTimelineId(schema), ...validateTimelineTitle(schema), + ...validateThreshold(schema), ]; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts index 0debe01e5a4d7..308b3c24010fb 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts @@ -28,6 +28,7 @@ import { To, type, Threat, + threshold, ThrottleOrNull, note, Version, @@ -106,6 +107,7 @@ export const createRulesSchema = t.intersection([ tags: DefaultStringArray, // defaults to empty string array if not set during decode to: DefaultToString, // defaults to "now" if not set during decode threat: DefaultThreatArray, // defaults to empty array if not set during decode + threshold, // defaults to "undefined" if not set during decode throttle: DefaultThrottleNull, // defaults to "null" if not set during decode timestamp_override, // defaults to "undefined" if not set during decode references: DefaultStringArray, // defaults to empty array of strings if not set during decode diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts index ebf0b2e591ca9..43f0901912271 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts @@ -65,4 +65,26 @@ describe('create_rules_type_dependents', () => { const errors = createRuleValidateTypeDependents(schema); expect(errors).toEqual(['when "timeline_title" exists, "timeline_id" must also exist']); }); + + test('threshold is required when type is threshold and validates with it', () => { + const schema: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + type: 'threshold', + }; + const errors = createRuleValidateTypeDependents(schema); + expect(errors).toEqual(['when "type" is "threshold", "threshold" is required']); + }); + + test('threshold.value is required and has to be bigger than 0 when type is threshold and validates with it', () => { + const schema: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + type: 'threshold', + threshold: { + field: '', + value: -1, + }, + }; + const errors = createRuleValidateTypeDependents(schema); + expect(errors).toEqual(['"threshold.value" has to be bigger than 0']); + }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts index aad2a2c4a9206..af665ff8c81d2 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts @@ -92,6 +92,19 @@ export const validateTimelineTitle = (rule: CreateRulesSchema): string[] => { return []; }; +export const validateThreshold = (rule: CreateRulesSchema): string[] => { + if (rule.type === 'threshold') { + if (!rule.threshold) { + return ['when "type" is "threshold", "threshold" is required']; + } else if (rule.threshold.value <= 0) { + return ['"threshold.value" has to be bigger than 0']; + } else { + return []; + } + } + return []; +}; + export const createRuleValidateTypeDependents = (schema: CreateRulesSchema): string[] => { return [ ...validateAnomalyThreshold(schema), @@ -101,5 +114,6 @@ export const createRuleValidateTypeDependents = (schema: CreateRulesSchema): str ...validateMachineLearningJobId(schema), ...validateTimelineId(schema), ...validateTimelineTitle(schema), + ...validateThreshold(schema), ]; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts index f61a1546e3e8a..d141ca56828b6 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts @@ -27,6 +27,7 @@ import { To, type, Threat, + threshold, ThrottleOrNull, note, Version, @@ -125,6 +126,7 @@ export const importRulesSchema = t.intersection([ tags: DefaultStringArray, // defaults to empty string array if not set during decode to: DefaultToString, // defaults to "now" if not set during decode threat: DefaultThreatArray, // defaults to empty array if not set during decode + threshold, // defaults to "undefined" if not set during decode throttle: DefaultThrottleNull, // defaults to "null" if not set during decode timestamp_override, // defaults to "undefined" if not set during decode references: DefaultStringArray, // defaults to empty array of strings if not set during decode diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_type_dependents.test.ts index f9b989c81e533..4b047ee6b7198 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_type_dependents.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_type_dependents.test.ts @@ -65,4 +65,26 @@ describe('import_rules_type_dependents', () => { const errors = importRuleValidateTypeDependents(schema); expect(errors).toEqual(['when "timeline_title" exists, "timeline_id" must also exist']); }); + + test('threshold is required when type is threshold and validates with it', () => { + const schema: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + type: 'threshold', + }; + const errors = importRuleValidateTypeDependents(schema); + expect(errors).toEqual(['when "type" is "threshold", "threshold" is required']); + }); + + test('threshold.value is required and has to be bigger than 0 when type is threshold and validates with it', () => { + const schema: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + type: 'threshold', + threshold: { + field: '', + value: -1, + }, + }; + const errors = importRuleValidateTypeDependents(schema); + expect(errors).toEqual(['"threshold.value" has to be bigger than 0']); + }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_type_dependents.ts index 59191a4fe3121..269181449e9e9 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_type_dependents.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_type_dependents.ts @@ -92,6 +92,19 @@ export const validateTimelineTitle = (rule: ImportRulesSchema): string[] => { return []; }; +export const validateThreshold = (rule: ImportRulesSchema): string[] => { + if (rule.type === 'threshold') { + if (!rule.threshold) { + return ['when "type" is "threshold", "threshold" is required']; + } else if (rule.threshold.value <= 0) { + return ['"threshold.value" has to be bigger than 0']; + } else { + return []; + } + } + return []; +}; + export const importRuleValidateTypeDependents = (schema: ImportRulesSchema): string[] => { return [ ...validateAnomalyThreshold(schema), @@ -101,5 +114,6 @@ export const importRuleValidateTypeDependents = (schema: ImportRulesSchema): str ...validateMachineLearningJobId(schema), ...validateTimelineId(schema), ...validateTimelineTitle(schema), + ...validateThreshold(schema), ]; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts index 070f3ccfd03b0..dd325c1a5034f 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts @@ -33,6 +33,7 @@ import { enabled, tags, threat, + threshold, throttle, references, to, @@ -89,6 +90,7 @@ export const patchRulesSchema = t.exact( tags, to, threat, + threshold, throttle, timestamp_override, references, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rule_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_type_dependents.test.ts similarity index 79% rename from x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rule_type_dependents.test.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_type_dependents.test.ts index a388e69332072..bafaf6f9e2203 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rule_type_dependents.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_type_dependents.test.ts @@ -78,4 +78,26 @@ describe('patch_rules_type_dependents', () => { const errors = patchRuleValidateTypeDependents(schema); expect(errors).toEqual(['either "id" or "rule_id" must be set']); }); + + test('threshold is required when type is threshold and validates with it', () => { + const schema: PatchRulesSchema = { + ...getPatchRulesSchemaMock(), + type: 'threshold', + }; + const errors = patchRuleValidateTypeDependents(schema); + expect(errors).toEqual(['when "type" is "threshold", "threshold" is required']); + }); + + test('threshold.value is required and has to be bigger than 0 when type is threshold and validates with it', () => { + const schema: PatchRulesSchema = { + ...getPatchRulesSchemaMock(), + type: 'threshold', + threshold: { + field: '', + value: -1, + }, + }; + const errors = patchRuleValidateTypeDependents(schema); + expect(errors).toEqual(['"threshold.value" has to be bigger than 0']); + }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_type_dependents.ts index 554cdb822762f..a229771a7c05c 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_type_dependents.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_type_dependents.ts @@ -66,6 +66,19 @@ export const validateId = (rule: PatchRulesSchema): string[] => { } }; +export const validateThreshold = (rule: PatchRulesSchema): string[] => { + if (rule.type === 'threshold') { + if (!rule.threshold) { + return ['when "type" is "threshold", "threshold" is required']; + } else if (rule.threshold.value <= 0) { + return ['"threshold.value" has to be bigger than 0']; + } else { + return []; + } + } + return []; +}; + export const patchRuleValidateTypeDependents = (schema: PatchRulesSchema): string[] => { return [ ...validateId(schema), @@ -73,5 +86,6 @@ export const patchRuleValidateTypeDependents = (schema: PatchRulesSchema): strin ...validateLanguage(schema), ...validateTimelineId(schema), ...validateTimelineTitle(schema), + ...validateThreshold(schema), ]; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts index 98082c2de838a..4f284eedef3fd 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts @@ -28,6 +28,7 @@ import { To, type, Threat, + threshold, ThrottleOrNull, note, version, @@ -114,6 +115,7 @@ export const updateRulesSchema = t.intersection([ tags: DefaultStringArray, // defaults to empty string array if not set during decode to: DefaultToString, // defaults to "now" if not set during decode threat: DefaultThreatArray, // defaults to empty array if not set during decode + threshold, // defaults to "undefined" if not set during decode throttle: DefaultThrottleNull, // defaults to "null" if not set during decode timestamp_override, // defaults to "undefined" if not set during decode references: DefaultStringArray, // defaults to empty array of strings if not set during decode diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.test.ts index a63c8243cb5f1..91b11ea758e93 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.test.ts @@ -85,4 +85,26 @@ describe('update_rules_type_dependents', () => { const errors = updateRuleValidateTypeDependents(schema); expect(errors).toEqual(['either "id" or "rule_id" must be set']); }); + + test('threshold is required when type is threshold and validates with it', () => { + const schema: UpdateRulesSchema = { + ...getUpdateRulesSchemaMock(), + type: 'threshold', + }; + const errors = updateRuleValidateTypeDependents(schema); + expect(errors).toEqual(['when "type" is "threshold", "threshold" is required']); + }); + + test('threshold.value is required and has to be bigger than 0 when type is threshold and validates with it', () => { + const schema: UpdateRulesSchema = { + ...getUpdateRulesSchemaMock(), + type: 'threshold', + threshold: { + field: '', + value: -1, + }, + }; + const errors = updateRuleValidateTypeDependents(schema); + expect(errors).toEqual(['"threshold.value" has to be bigger than 0']); + }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts index 9204f727b2660..44182d250c801 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts @@ -102,6 +102,19 @@ export const validateId = (rule: UpdateRulesSchema): string[] => { } }; +export const validateThreshold = (rule: UpdateRulesSchema): string[] => { + if (rule.type === 'threshold') { + if (!rule.threshold) { + return ['when "type" is "threshold", "threshold" is required']; + } else if (rule.threshold.value <= 0) { + return ['"threshold.value" has to be bigger than 0']; + } else { + return []; + } + } + return []; +}; + export const updateRuleValidateTypeDependents = (schema: UpdateRulesSchema): string[] => { return [ ...validateId(schema), @@ -112,5 +125,6 @@ export const updateRuleValidateTypeDependents = (schema: UpdateRulesSchema): str ...validateMachineLearningJobId(schema), ...validateTimelineId(schema), ...validateTimelineTitle(schema), + ...validateThreshold(schema), ]; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts index c0fec2b2eefc2..4bd18a13e4ebb 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts @@ -44,6 +44,7 @@ import { timeline_title, type, threat, + threshold, throttle, job_status, status_date, @@ -123,6 +124,9 @@ export const dependentRulesSchema = t.partial({ // ML fields anomaly_threshold, machine_learning_job_id, + + // Threshold fields + threshold, }); /** @@ -202,7 +206,7 @@ export const addTimelineTitle = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mi }; export const addQueryFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { - if (typeAndTimelineOnly.type === 'query' || typeAndTimelineOnly.type === 'saved_query') { + if (['query', 'saved_query', 'threshold'].includes(typeAndTimelineOnly.type)) { return [ t.exact(t.type({ query: dependentRulesSchema.props.query })), t.exact(t.type({ language: dependentRulesSchema.props.language })), @@ -225,6 +229,17 @@ export const addMlFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] } }; +export const addThresholdFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { + if (typeAndTimelineOnly.type === 'threshold') { + return [ + t.exact(t.type({ threshold: dependentRulesSchema.props.threshold })), + t.exact(t.partial({ saved_id: dependentRulesSchema.props.saved_id })), + ]; + } else { + return []; + } +}; + export const getDependents = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed => { const dependents: t.Mixed[] = [ t.exact(requiredRulesSchema), @@ -233,6 +248,7 @@ export const getDependents = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed ...addTimelineTitle(typeAndTimelineOnly), ...addQueryFields(typeAndTimelineOnly), ...addMlFields(typeAndTimelineOnly), + ...addThresholdFields(typeAndTimelineOnly), ]; if (dependents.length > 1) { diff --git a/x-pack/plugins/security_solution/common/detection_engine/types.ts b/x-pack/plugins/security_solution/common/detection_engine/types.ts index 431d716a9f205..7c752bca49dbd 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/types.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/types.ts @@ -15,5 +15,6 @@ export const RuleTypeSchema = t.keyof({ query: null, saved_query: null, machine_learning: null, + threshold: null, }); export type RuleType = t.TypeOf; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 1213312e2a22c..24bfeaa4dae1a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -53,6 +53,7 @@ describe('alert actions', () => { apolloClient, createTimeline, ecsData: mockEcsDataWithAlert, + nonEcsData: [], updateTimelineIsLoading, }); @@ -65,6 +66,7 @@ describe('alert actions', () => { apolloClient, createTimeline, ecsData: mockEcsDataWithAlert, + nonEcsData: [], updateTimelineIsLoading, }); const expected = { @@ -250,6 +252,7 @@ describe('alert actions', () => { apolloClient, createTimeline, ecsData: mockEcsDataWithAlert, + nonEcsData: [], updateTimelineIsLoading, }); // @ts-ignore @@ -279,6 +282,7 @@ describe('alert actions', () => { apolloClient, createTimeline, ecsData: mockEcsDataWithAlert, + nonEcsData: [], updateTimelineIsLoading, }); // @ts-ignore @@ -297,6 +301,7 @@ describe('alert actions', () => { apolloClient, createTimeline, ecsData: mockEcsDataWithAlert, + nonEcsData: [], updateTimelineIsLoading, }); @@ -326,6 +331,7 @@ describe('alert actions', () => { apolloClient, createTimeline, ecsData: ecsDataMock, + nonEcsData: [], updateTimelineIsLoading, }); @@ -350,6 +356,7 @@ describe('alert actions', () => { await sendAlertToTimelineAction({ createTimeline, ecsData: ecsDataMock, + nonEcsData: [], updateTimelineIsLoading, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index 24f292cf9135b..11c13c2358e94 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable complexity */ + import dateMath from '@elastic/datemath'; -import { getOr, isEmpty } from 'lodash/fp'; +import { get, getOr, isEmpty, find } from 'lodash/fp'; import moment from 'moment'; import { updateAlertStatus } from '../../containers/detection_engine/alerts/api'; @@ -30,6 +32,8 @@ import { replaceTemplateFieldFromMatchFilters, replaceTemplateFieldFromDataProviders, } from './helpers'; +import { KueryFilterQueryKind } from '../../../common/store'; +import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; export const getUpdateAlertsQuery = (eventIds: Readonly) => { return { @@ -99,10 +103,45 @@ export const determineToAndFrom = ({ ecsData }: { ecsData: Ecs }) => { return { to, from }; }; +export const getThresholdAggregationDataProvider = ( + ecsData: Ecs, + nonEcsData: TimelineNonEcsData[] +): DataProvider[] => { + const aggregationField = ecsData.signal?.rule?.threshold.field; + const aggregationValue = + get(aggregationField, ecsData) ?? find(['field', aggregationField], nonEcsData)?.value; + const dataProviderValue = Array.isArray(aggregationValue) + ? aggregationValue[0] + : aggregationValue; + + if (!dataProviderValue) { + return []; + } + + const aggregationFieldId = aggregationField.replace('.', '-'); + + return [ + { + and: [], + id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-${aggregationFieldId}-${dataProviderValue}`, + name: ecsData.signal?.rule?.threshold.field, + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: aggregationField, + value: dataProviderValue, + operator: ':', + }, + }, + ]; +}; + export const sendAlertToTimelineAction = async ({ apolloClient, createTimeline, ecsData, + nonEcsData, updateTimelineIsLoading, }: SendAlertToTimelineActionProps) => { let openAlertInBasicTimeline = true; @@ -146,7 +185,7 @@ export const sendAlertToTimelineAction = async ({ timeline.timelineType ); - createTimeline({ + return createTimeline({ from, timeline: { ...timeline, @@ -186,8 +225,62 @@ export const sendAlertToTimelineAction = async ({ } } - if (openAlertInBasicTimeline) { - createTimeline({ + if ( + ecsData.signal?.rule?.type?.length && + ecsData.signal?.rule?.type[0] === 'threshold' && + openAlertInBasicTimeline + ) { + return createTimeline({ + from, + timeline: { + ...timelineDefaults, + dataProviders: [ + { + and: [], + id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-alert-id-${ecsData._id}`, + name: ecsData._id, + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: '_id', + value: ecsData._id, + operator: ':', + }, + }, + ...getThresholdAggregationDataProvider(ecsData, nonEcsData), + ], + id: 'timeline-1', + dateRange: { + start: from, + end: to, + }, + eventType: 'all', + kqlQuery: { + filterQuery: { + kuery: { + kind: ecsData.signal?.rule?.language?.length + ? (ecsData.signal?.rule?.language[0] as KueryFilterQueryKind) + : 'kuery', + expression: ecsData.signal?.rule?.query?.length ? ecsData.signal?.rule?.query[0] : '', + }, + serializedQuery: ecsData.signal?.rule?.query?.length + ? ecsData.signal?.rule?.query[0] + : '', + }, + filterQueryDraft: { + kind: ecsData.signal?.rule?.language?.length + ? (ecsData.signal?.rule?.language[0] as KueryFilterQueryKind) + : 'kuery', + expression: ecsData.signal?.rule?.query?.length ? ecsData.signal?.rule?.query[0] : '', + }, + }, + }, + to, + ruleNote: noteContent, + }); + } else { + return createTimeline({ from, timeline: { ...timelineDefaults, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index 319575c9c307f..6f1f2e46dce3d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -309,11 +309,12 @@ export const getAlertActions = ({ displayType: 'icon', iconType: 'timeline', id: 'sendAlertToTimeline', - onClick: ({ ecsData }: TimelineRowActionOnClick) => + onClick: ({ ecsData, data }: TimelineRowActionOnClick) => sendAlertToTimelineAction({ apolloClient, createTimeline, ecsData, + nonEcsData: data, updateTimelineIsLoading, }), width: DEFAULT_ICON_BUTTON_WIDTH, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts index b127ff04eca46..34d18b4dedba6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts @@ -7,7 +7,7 @@ import ApolloClient from 'apollo-client'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; -import { Ecs } from '../../../graphql/types'; +import { Ecs, TimelineNonEcsData } from '../../../graphql/types'; import { TimelineModel } from '../../../timelines/store/timeline/model'; import { inputsModel } from '../../../common/store'; @@ -53,6 +53,7 @@ export interface SendAlertToTimelineActionProps { apolloClient?: ApolloClient<{}>; createTimeline: CreateTimeline; ecsData: Ecs; + nonEcsData: TimelineNonEcsData[]; updateTimelineIsLoading: UpdateTimelineLoading; } diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx index b82d1c0a36ab2..41ee91845a8ec 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx @@ -403,5 +403,17 @@ describe('helpers', () => { expect(result.description).toEqual('Query'); }); + + it('returns the label for a threshold type', () => { + const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'threshold'); + + expect(result.title).toEqual('Test label'); + }); + + it('returns a humanized description for a threshold type', () => { + const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'threshold'); + + expect(result.description).toEqual('Threshold'); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx index a0d43c3abf5c1..8393f2230dcfe 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx @@ -19,6 +19,7 @@ import { isEmpty } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; +import { Threshold } from '../../../../../common/detection_engine/schemas/common/schemas'; import { RuleType } from '../../../../../common/detection_engine/types'; import { esFilters } from '../../../../../../../../src/plugins/data/public'; @@ -132,10 +133,10 @@ export const buildThreatDescription = ({ label, threat }: BuildThreatDescription {tactic != null ? tactic.text : ''} - {singleThreat.technique.map((technique) => { + {singleThreat.technique.map((technique, listIndex) => { const myTechnique = techniquesOptions.find((t) => t.id === technique.id); return ( - + [ + { + title: label, + description: ( + <> + {isEmpty(threshold.field[0]) + ? `${i18n.THRESHOLD_RESULTS_ALL} >= ${threshold.value}` + : `${i18n.THRESHOLD_RESULTS_AGGREGATED_BY} ${threshold.field[0]} >= ${threshold.value}`} + + ), + }, +]; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx index 0a7e666d65aef..5a2a44a284e3b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, mount } from 'enzyme'; import { StepRuleDescriptionComponent, @@ -367,6 +367,52 @@ describe('description_step', () => { }); }); + describe('threshold', () => { + test('returns threshold description when threshold exist and field is empty', () => { + const mockThreshold = { + isNew: false, + threshold: { + field: [''], + value: 100, + }, + }; + const result: ListItems[] = getDescriptionItem( + 'threshold', + 'Threshold label', + mockThreshold, + mockFilterManager + ); + + expect(result[0].title).toEqual('Threshold label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + expect(mount(result[0].description as React.ReactElement).html()).toContain( + 'All results >= 100' + ); + }); + + test('returns threshold description when threshold exist and field is set', () => { + const mockThreshold = { + isNew: false, + threshold: { + field: ['user.name'], + value: 100, + }, + }; + const result: ListItems[] = getDescriptionItem( + 'threshold', + 'Threshold label', + mockThreshold, + mockFilterManager + ); + + expect(result[0].title).toEqual('Threshold label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + expect(mount(result[0].description as React.ReactElement).html()).toContain( + 'Results aggregated by user.name >= 100' + ); + }); + }); + describe('references', () => { test('returns array of ListItems when references exist', () => { const result: ListItems[] = getDescriptionItem( diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index 8f3a76c6aea57..51624d04cb58b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -35,6 +35,7 @@ import { buildUrlsDescription, buildNoteDescription, buildRuleTypeDescription, + buildThresholdDescription, } from './helpers'; import { useSiemJobs } from '../../../../common/components/ml_popover/hooks/use_siem_jobs'; import { buildMlJobDescription } from './ml_job_description'; @@ -179,6 +180,9 @@ export const getDescriptionItem = ( (singleThreat: IMitreEnterpriseAttack) => singleThreat.tactic.name !== 'none' ); return buildThreatDescription({ label, threat }); + } else if (field === 'threshold') { + const threshold = get(field, data); + return buildThresholdDescription(label, threshold); } else if (field === 'references') { const urls: string[] = get(field, data); return buildUrlsDescription(label, urls); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx index 3e639ede7a18b..76217964a87cb 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx @@ -41,6 +41,13 @@ export const QUERY_TYPE_DESCRIPTION = i18n.translate( } ); +export const THRESHOLD_TYPE_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.thresholdRuleTypeDescription', + { + defaultMessage: 'Threshold', + } +); + export const ML_JOB_STARTED = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDescription.mlJobStartedDescription', { @@ -54,3 +61,17 @@ export const ML_JOB_STOPPED = i18n.translate( defaultMessage: 'Stopped', } ); + +export const THRESHOLD_RESULTS_ALL = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.thresholdResultsAllDescription', + { + defaultMessage: 'All results', + } +); + +export const THRESHOLD_RESULTS_AGGREGATED_BY = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.thresholdResultsAggregatedByDescription', + { + defaultMessage: 'Results aggregated by', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx index 3dad53f532a5b..6546c1ba59d84 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx @@ -4,52 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiCard, - EuiFlexGrid, - EuiFlexItem, - EuiFormRow, - EuiIcon, - EuiLink, - EuiText, -} from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { EuiCard, EuiFlexGrid, EuiFlexItem, EuiFormRow, EuiIcon } from '@elastic/eui'; import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { RuleType } from '../../../../../common/detection_engine/types'; import { FieldHook } from '../../../../shared_imports'; import { useKibana } from '../../../../common/lib/kibana'; import * as i18n from './translations'; +import { MlCardDescription } from './ml_card_description'; -const MlCardDescription = ({ - subscriptionUrl, - hasValidLicense = false, -}: { - subscriptionUrl: string; - hasValidLicense?: boolean; -}) => ( - - {hasValidLicense ? ( - i18n.ML_TYPE_DESCRIPTION - ) : ( - - - - ), - }} - /> - )} - -); +const isThresholdRule = (ruleType: RuleType) => ruleType === 'threshold'; interface SelectRuleTypeProps { describedByIds?: string[]; @@ -75,11 +40,39 @@ export const SelectRuleType: React.FC = ({ ); const setMl = useCallback(() => setType('machine_learning'), [setType]); const setQuery = useCallback(() => setType('query'), [setType]); + const setThreshold = useCallback(() => setType('threshold'), [setType]); const mlCardDisabled = isReadOnly || !hasValidLicense || !isMlAdmin; const licensingUrl = useKibana().services.application.getUrlForApp('kibana', { path: '#/management/stack/license_management', }); + const querySelectableConfig = useMemo( + () => ({ + isDisabled: isReadOnly, + onClick: setQuery, + isSelected: !isMlRule(ruleType) && !isThresholdRule(ruleType), + }), + [isReadOnly, ruleType, setQuery] + ); + + const mlSelectableConfig = useMemo( + () => ({ + isDisabled: mlCardDisabled, + onClick: setMl, + isSelected: isMlRule(ruleType), + }), + [mlCardDisabled, ruleType, setMl] + ); + + const thresholdSelectableConfig = useMemo( + () => ({ + isDisabled: isReadOnly, + onClick: setThreshold, + isSelected: isThresholdRule(ruleType), + }), + [isReadOnly, ruleType, setThreshold] + ); + return ( = ({ title={i18n.QUERY_TYPE_TITLE} description={i18n.QUERY_TYPE_DESCRIPTION} icon={} - selectable={{ - isDisabled: isReadOnly, - onClick: setQuery, - isSelected: !isMlRule(ruleType), - }} + isDisabled={querySelectableConfig.isDisabled && !querySelectableConfig.isSelected} + selectable={querySelectableConfig} /> @@ -109,12 +99,20 @@ export const SelectRuleType: React.FC = ({ } icon={} - isDisabled={mlCardDisabled} - selectable={{ - isDisabled: mlCardDisabled, - onClick: setMl, - isSelected: isMlRule(ruleType), - }} + isDisabled={mlSelectableConfig.isDisabled && !mlSelectableConfig.isSelected} + selectable={mlSelectableConfig} + /> + + + } + isDisabled={ + thresholdSelectableConfig.isDisabled && !thresholdSelectableConfig.isSelected + } + selectable={thresholdSelectableConfig} /> diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/ml_card_description.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/ml_card_description.tsx new file mode 100644 index 0000000000000..2171c93e47d63 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/ml_card_description.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiText, EuiLink } from '@elastic/eui'; +import React from 'react'; + +import { ML_TYPE_DESCRIPTION } from './translations'; + +interface MlCardDescriptionProps { + subscriptionUrl: string; + hasValidLicense?: boolean; +} + +const MlCardDescriptionComponent: React.FC = ({ + subscriptionUrl, + hasValidLicense = false, +}) => ( + + {hasValidLicense ? ( + ML_TYPE_DESCRIPTION + ) : ( + + + + ), + }} + /> + )} + +); + +MlCardDescriptionComponent.displayName = 'MlCardDescriptionComponent'; + +export const MlCardDescription = React.memo(MlCardDescriptionComponent); + +MlCardDescription.displayName = 'MlCardDescription'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/translations.ts index 8b92d20616f7c..3b85a7dfc765c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/translations.ts @@ -33,3 +33,17 @@ export const ML_TYPE_DESCRIPTION = i18n.translate( defaultMessage: 'Select ML job to detect anomalous activity.', } ); + +export const THRESHOLD_TYPE_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.thresholdTypeTitle', + { + defaultMessage: 'Threshold', + } +); + +export const THRESHOLD_TYPE_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.thresholdTypeDescription', + { + defaultMessage: 'Aggregate query results to detect when number of matches exceeds threshold.', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index 864f953bff1e1..c7d70684b34cf 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -35,12 +35,14 @@ import { MlJobSelect } from '../ml_job_select'; import { PickTimeline } from '../pick_timeline'; import { StepContentWrapper } from '../step_content_wrapper'; import { NextStep } from '../next_step'; +import { ThresholdInput } from '../threshold_input'; import { Field, Form, - FormDataProvider, getUseField, UseField, + UseMultiFields, + FormDataProvider, useForm, FormSchema, } from '../../../../shared_imports'; @@ -64,6 +66,10 @@ const stepDefineDefaultValue: DefineStepRule = { filters: [], saved_id: undefined, }, + threshold: { + field: [], + value: '200', + }, timeline: { id: null, title: DEFAULT_TIMELINE_TITLE, @@ -84,6 +90,12 @@ MyLabelButton.defaultProps = { flush: 'right', }; +const RuleTypeEuiFormRow = styled(EuiFormRow).attrs<{ $isVisible: boolean }>(({ $isVisible }) => ({ + style: { + display: $isVisible ? 'flex' : 'none', + }, +}))<{ $isVisible: boolean }>``; + const StepDefineRuleComponent: FC = ({ addPadding = false, defaultValues, @@ -97,7 +109,9 @@ const StepDefineRuleComponent: FC = ({ const mlCapabilities = useMlCapabilities(); const [openTimelineSearch, setOpenTimelineSearch] = useState(false); const [indexModified, setIndexModified] = useState(false); - const [localIsMlRule, setIsMlRule] = useState(false); + const [localRuleType, setLocalRuleType] = useState( + defaultValues?.ruleType || stepDefineDefaultValue.ruleType + ); const [indicesConfig] = useUiSetting$(DEFAULT_INDEX_KEY); const [myStepData, setMyStepData] = useState({ ...stepDefineDefaultValue, @@ -156,6 +170,17 @@ const StepDefineRuleComponent: FC = ({ setOpenTimelineSearch(false); }, []); + const ThresholdInputChildren = useCallback( + ({ thresholdField, thresholdValue }) => ( + + ), + [browserFields] + ); + return isReadOnlyView ? ( = ({ isMlAdmin: hasMlAdminPermissions(mlCapabilities), }} /> - + <> = ({ }} /> - - + + <> = ({ }} /> - + + + <> + + {ThresholdInputChildren} + + + = ({ } else if (!deepEqual(index, indicesConfig) && !indexModified) { setIndexModified(true); } + if (myStepData.index !== index) { + setMyStepData((prevValue) => ({ ...prevValue, index })); + } } - if (isMlRule(ruleType) && !localIsMlRule) { - setIsMlRule(true); - clearErrors(); - } else if (!isMlRule(ruleType) && localIsMlRule) { - setIsMlRule(false); + if (ruleType !== localRuleType) { + setLocalRuleType(ruleType); clearErrors(); } - return null; }} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx index 190d4484b156b..67d795ccf90f0 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx @@ -172,4 +172,36 @@ export const schema: FormSchema = { } ), }, + threshold: { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdLabel', + { + defaultMessage: 'Threshold', + } + ), + field: { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdFieldLabel', + { + defaultMessage: 'Field', + } + ), + helpText: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdFieldHelpText', + { + defaultMessage: 'Select a field to group results by', + } + ), + }, + value: { + type: FIELD_TYPES.NUMBER, + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdValueLabel', + { + defaultMessage: 'Threshold', + } + ), + }, + }, }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/threshold_input/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/threshold_input/index.tsx new file mode 100644 index 0000000000000..81e771ce4dc5b --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/threshold_input/index.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; + +import { BrowserFields } from '../../../../common/containers/source'; +import { getCategorizedFieldNames } from '../../../../timelines/components/edit_data_provider/helpers'; +import { FieldHook, Field } from '../../../../shared_imports'; +import { THRESHOLD_FIELD_PLACEHOLDER } from './translations'; + +const FIELD_COMBO_BOX_WIDTH = 410; + +export interface FieldValueThreshold { + field: string[]; + value: string; +} + +interface ThresholdInputProps { + thresholdField: FieldHook; + thresholdValue: FieldHook; + browserFields: BrowserFields; +} + +const OperatorWrapper = styled(EuiFlexItem)` + align-self: center; +`; + +const fieldDescribedByIds = ['detectionEngineStepDefineRuleThresholdField']; +const valueDescribedByIds = ['detectionEngineStepDefineRuleThresholdValue']; + +const ThresholdInputComponent: React.FC = ({ + thresholdField, + thresholdValue, + browserFields, +}: ThresholdInputProps) => { + const fieldEuiFieldProps = useMemo( + () => ({ + fullWidth: true, + singleSelection: { asPlainText: true }, + noSuggestions: false, + options: getCategorizedFieldNames(browserFields), + placeholder: THRESHOLD_FIELD_PLACEHOLDER, + onCreateOption: undefined, + style: { width: `${FIELD_COMBO_BOX_WIDTH}px` }, + }), + [browserFields] + ); + + return ( + + + + + {'>='} + + + + + ); +}; + +export const ThresholdInput = React.memo(ThresholdInputComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/threshold_input/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/threshold_input/translations.ts new file mode 100644 index 0000000000000..228848ef12130 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/threshold_input/translations.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const THRESHOLD_FIELD_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.thresholdField.thresholdFieldPlaceholderText', + { + defaultMessage: 'All results', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index 5c876625cf9f9..1f75ff0210bd5 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -16,6 +16,7 @@ import { rule_name_override, severity_mapping, timestamp_override, + threshold, } from '../../../../../common/detection_engine/schemas/common/schemas'; /* eslint-enable @typescript-eslint/camelcase */ import { @@ -65,6 +66,7 @@ export const NewRuleSchema = t.intersection([ saved_id: t.string, tags: t.array(t.string), threat: t.array(t.unknown), + threshold, throttle: t.union([t.string, t.null]), to: t.string, updated_by: t.string, @@ -142,6 +144,7 @@ export const RuleSchema = t.intersection([ saved_id: t.string, status: t.string, status_date: t.string, + threshold, timeline_id: t.string, timeline_title: t.string, timestamp_override, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts index 2b86abf4255c6..5d84cf5314029 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts @@ -153,6 +153,10 @@ export const mockRuleWithEverything = (id: string): Rule => ({ ], }, ], + threshold: { + field: 'host.name', + value: 50, + }, throttle: 'no_actions', timestamp_override: 'event.ingested', note: '# this is some markdown documentation', @@ -213,6 +217,10 @@ export const mockDefineStepRule = (isNew = false): DefineStepRule => ({ id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', title: 'Titled timeline', }, + threshold: { + field: [''], + value: '100', + }, }); export const mockScheduleStepRule = (isNew = false): ScheduleStepRule => ({ diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index 8331346b19ac9..4bb7196e17db5 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -51,19 +51,29 @@ export interface RuleFields { queryBar: unknown; index: unknown; ruleType: unknown; + threshold?: unknown; } -type QueryRuleFields = Omit; +type QueryRuleFields = Omit; +type ThresholdRuleFields = Omit; type MlRuleFields = Omit; -const isMlFields = (fields: QueryRuleFields | MlRuleFields): fields is MlRuleFields => - has('anomalyThreshold', fields); +const isMlFields = ( + fields: QueryRuleFields | MlRuleFields | ThresholdRuleFields +): fields is MlRuleFields => has('anomalyThreshold', fields); + +const isThresholdFields = ( + fields: QueryRuleFields | MlRuleFields | ThresholdRuleFields +): fields is ThresholdRuleFields => has('threshold', fields); export const filterRuleFieldsForType = (fields: T, type: RuleType) => { if (isMlRule(type)) { const { index, queryBar, ...mlRuleFields } = fields; return mlRuleFields; + } else if (type === 'threshold') { + const { anomalyThreshold, machineLearningJobId, ...thresholdRuleFields } = fields; + return thresholdRuleFields; } else { - const { anomalyThreshold, machineLearningJobId, ...queryRuleFields } = fields; + const { anomalyThreshold, machineLearningJobId, threshold, ...queryRuleFields } = fields; return queryRuleFields; } }; @@ -85,6 +95,20 @@ export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStep anomaly_threshold: ruleFields.anomalyThreshold, machine_learning_job_id: ruleFields.machineLearningJobId, } + : isThresholdFields(ruleFields) + ? { + index: ruleFields.index, + filters: ruleFields.queryBar?.filters, + language: ruleFields.queryBar?.query?.language, + query: ruleFields.queryBar?.query?.query as string, + saved_id: ruleFields.queryBar?.saved_id, + ...(ruleType === 'threshold' && { + threshold: { + field: ruleFields.threshold?.field[0] ?? '', + value: parseInt(ruleFields.threshold?.value, 10) ?? 0, + }, + }), + } : { index: ruleFields.index, filters: ruleFields.queryBar?.filters, 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 f8969f06c8ef6..590643f8236ee 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 @@ -74,6 +74,10 @@ describe('rule helpers', () => { ], saved_id: 'test123', }, + threshold: { + field: ['host.name'], + value: '50', + }, timeline: { id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', title: 'Titled timeline', @@ -206,6 +210,10 @@ describe('rule helpers', () => { filters: [], saved_id: "Garrett's IP", }, + threshold: { + field: [], + value: '100', + }, timeline: { id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', title: 'Untitled timeline', @@ -235,6 +243,10 @@ describe('rule helpers', () => { filters: [], saved_id: undefined, }, + threshold: { + field: [], + value: '100', + }, timeline: { id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', title: 'Untitled timeline', 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 ce37b02a0b5ae..6541b92f575c1 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 @@ -84,6 +84,10 @@ export const getDefineStepsData = (rule: Rule): DefineStepRule => ({ id: rule.timeline_id ?? null, title: rule.timeline_title ?? null, }, + threshold: { + field: rule.threshold?.field ? [rule.threshold.field] : [], + value: `${rule.threshold?.value || 100}`, + }, }); export const getScheduleStepsData = (rule: Rule): ScheduleStepRule => { @@ -290,6 +294,20 @@ export const redirectToDetections = ( hasEncryptionKey === false || needsListsConfiguration; +const getRuleSpecificRuleParamKeys = (ruleType: RuleType) => { + const queryRuleParams = ['index', 'filters', 'language', 'query', 'saved_id']; + + if (isMlRule(ruleType)) { + return ['anomaly_threshold', 'machine_learning_job_id']; + } + + if (ruleType === 'threshold') { + return ['threshold', ...queryRuleParams]; + } + + return queryRuleParams; +}; + export const getActionMessageRuleParams = (ruleType: RuleType): string[] => { const commonRuleParamsKeys = [ 'id', @@ -312,9 +330,7 @@ export const getActionMessageRuleParams = (ruleType: RuleType): string[] => { const ruleParamsKeys = [ ...commonRuleParamsKeys, - ...(isMlRule(ruleType) - ? ['anomaly_threshold', 'machine_learning_job_id'] - : ['index', 'filters', 'language', 'query', 'saved_id']), + ...getRuleSpecificRuleParamKeys(ruleType), ].sort(); return ruleParamsKeys; 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 f453b5a95994d..e7daff0947b0d 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 @@ -10,6 +10,7 @@ import { Filter } from '../../../../../../../../src/plugins/data/common'; import { FormData, FormHook } from '../../../../shared_imports'; import { FieldValueQueryBar } from '../../../components/rules/query_bar'; import { FieldValueTimeline } from '../../../components/rules/pick_timeline'; +import { FieldValueThreshold } from '../../../components/rules/threshold_input'; import { Author, BuildingBlockType, @@ -99,6 +100,7 @@ export interface DefineStepRule extends StepRuleData { queryBar: FieldValueQueryBar; ruleType: RuleType; timeline: FieldValueTimeline; + threshold: FieldValueThreshold; } export interface ScheduleStepRule extends StepRuleData { @@ -122,6 +124,10 @@ export interface DefineStepRuleJson { saved_id?: string; query?: string; language?: string; + threshold?: { + field: string; + value: number; + }; timeline_id?: string; timeline_title?: string; type: RuleType; diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index d5fbf4d865ac5..43c478ff120a0 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -4750,6 +4750,14 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "threshold", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToAny", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "exceptions_list", "description": "", diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 429590ffc3e7d..084d1a63fec75 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -1070,6 +1070,8 @@ export interface RuleField { note?: Maybe; + threshold?: Maybe; + exceptions_list?: Maybe; } @@ -5066,6 +5068,10 @@ export namespace GetTimelineQuery { note: Maybe; + type: Maybe; + + threshold: Maybe; + exceptions_list: Maybe; }; diff --git a/x-pack/plugins/security_solution/public/shared_imports.ts b/x-pack/plugins/security_solution/public/shared_imports.ts index fcd23ff9df4d8..5d4579b427f18 100644 --- a/x-pack/plugins/security_solution/public/shared_imports.ts +++ b/x-pack/plugins/security_solution/public/shared_imports.ts @@ -18,6 +18,7 @@ export { FormHook, FormSchema, UseField, + UseMultiFields, useForm, ValidationFunc, VALIDATION_TYPES, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index 125ba23a5c5a5..c9c8250922161 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -96,7 +96,7 @@ export const Actions = React.memo( data-test-subj="event-actions-container" > {showCheckboxes && ( - + {loadingEventIds.includes(eventId) ? ( @@ -117,7 +117,7 @@ export const Actions = React.memo( )} - + {loading ? ( @@ -137,7 +137,7 @@ export const Actions = React.memo( {!isEventViewer && ( <> - + ( - + ( ...acc, icon: [ ...acc.icon, - - + + ( return grouped.contextMenu.length > 0 ? [ ...grouped.icon, - - + + ( : grouped.icon; }, [button, closePopover, id, onClickCb, data, ecsData, timelineActions, isPopoverOpen]); + const handlePinClicked = useCallback( + () => + getPinOnClick({ + allowUnpinning: !eventHasNotes(eventIdToNoteIds[id]), + eventId: id, + onPinEvent, + onUnPinEvent, + isEventPinned, + }), + [eventIdToNoteIds, id, isEventPinned, onPinEvent, onUnPinEvent] + ); + return ( ( loadingEventIds={loadingEventIds} noteIds={eventIdToNoteIds[id] || emptyNotes} onEventToggled={onEventToggled} - onPinClicked={getPinOnClick({ - allowUnpinning: !eventHasNotes(eventIdToNoteIds[id]), - eventId: id, - onPinEvent, - onUnPinEvent, - isEventPinned, - })} + onPinClicked={handlePinClicked} showCheckboxes={showCheckboxes} showNotes={showNotes} toggleShowNotes={toggleShowNotes} diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts index 2624532b78d4d..6c90b39a8e688 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts @@ -210,6 +210,8 @@ export const timelineQuery = gql` to filters note + type + threshold exceptions_list } } diff --git a/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts index f8afbae840d08..5b093a02b6514 100644 --- a/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts @@ -416,6 +416,7 @@ export const ecsSchema = gql` updated_by: ToStringArray version: ToStringArray note: ToStringArray + threshold: ToAny exceptions_list: ToAny } diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index b44a8f5cceaf1..668266cc67c3a 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -1072,6 +1072,8 @@ export interface RuleField { note?: Maybe; + threshold?: Maybe; + exceptions_list?: Maybe; } @@ -4939,6 +4941,8 @@ export namespace RuleFieldResolvers { note?: NoteResolver, TypeParent, TContext>; + threshold?: ThresholdResolver, TypeParent, TContext>; + exceptions_list?: ExceptionsListResolver, TypeParent, TContext>; } @@ -5097,6 +5101,11 @@ export namespace RuleFieldResolvers { Parent = RuleField, TContext = SiemContext > = Resolver; + export type ThresholdResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; export type ExceptionsListResolver< R = Maybe, Parent = RuleField, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 9ca102b437511..29c56e8ed80b1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -394,6 +394,7 @@ export const getResult = (): RuleAlertType => ({ ], }, ], + threshold: undefined, timestampOverride: undefined, references: ['http://www.example.com', 'https://ww.example.com'], note: '# Investigative notes', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json index d600bae2746d9..7d80a319e9e52 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json @@ -174,6 +174,16 @@ } } }, + "threshold": { + "properties": { + "field": { + "type": "keyword" + }, + "value": { + "type": "float" + } + } + }, "note": { "type": "text" }, @@ -286,6 +296,9 @@ }, "status": { "type": "keyword" + }, + "threshold_count": { + "type": "float" } } } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 2942413057e37..acd800e54040c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -90,6 +90,7 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => severity_mapping: severityMapping, tags, threat, + threshold, throttle, timestamp_override: timestampOverride, to, @@ -177,6 +178,7 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => to, type, threat, + threshold, timestampOverride, references, note, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts index 310a9da56282d..edad3dd8a4f21 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -44,6 +44,7 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void if (validationErrors.length) { return siemResponse.error({ statusCode: 400, body: validationErrors }); } + const { actions: actionsRest, anomaly_threshold: anomalyThreshold, @@ -75,6 +76,7 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void severity_mapping: severityMapping, tags, threat, + threshold, throttle, timestamp_override: timestampOverride, to, @@ -125,6 +127,7 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void }); } } + const createdRule = await createRules({ alertsClient, anomalyThreshold, @@ -159,6 +162,7 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void to, type, threat, + threshold, timestampOverride, references, note, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts index 43aa1ecd31922..18eea7c45585f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -161,6 +161,7 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP severity_mapping: severityMapping, tags, threat, + threshold, timestamp_override: timestampOverride, to, type, @@ -222,6 +223,7 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP to, type, threat, + threshold, timestampOverride, references, note, @@ -264,6 +266,7 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP to, type, threat, + threshold, references, note, version, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index c3d6f920e47a9..5099cf5de958f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -85,6 +85,7 @@ export const patchRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => to, type, threat, + threshold, timestamp_override: timestampOverride, throttle, references, @@ -143,6 +144,7 @@ export const patchRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => to, type, threat, + threshold, timestampOverride, references, note, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts index eb9624e6412e9..3b3efd2ed166d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -76,6 +76,7 @@ export const patchRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { to, type, threat, + threshold, timestamp_override: timestampOverride, throttle, references, @@ -142,6 +143,7 @@ export const patchRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { to, type, threat, + threshold, timestampOverride, references, note, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index c1ab1be2dbd0a..518024387fed3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -88,6 +88,7 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => to, type, threat, + threshold, throttle, timestamp_override: timestampOverride, references, @@ -156,6 +157,7 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => to, type, threat, + threshold, timestampOverride, references, note, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts index 717f388cfc1e9..299b99c4d37b0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -78,6 +78,7 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { to, type, threat, + threshold, throttle, timestamp_override: timestampOverride, references, @@ -146,6 +147,7 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { to, type, threat, + threshold, timestampOverride, references, note, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts index 9e93dc051a041..ee83ea91578c5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts @@ -144,6 +144,7 @@ export const transformAlertToRule = ( to: alert.params.to, type: alert.params.type, threat: alert.params.threat ?? [], + threshold: alert.params.threshold, throttle: ruleActions?.ruleThrottle || 'no_actions', timestamp_override: alert.params.timestampOverride, note: alert.params.note, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts index a7e24a1ac1609..1117f34b6f8c5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts @@ -39,6 +39,7 @@ export const getCreateRulesOptionsMock = (): CreateRulesOptions => ({ severityMapping: [], tags: [], threat: [], + threshold: undefined, timestampOverride: undefined, to: 'now', type: 'query', @@ -81,6 +82,7 @@ export const getCreateMlRulesOptionsMock = (): CreateRulesOptions => ({ severityMapping: [], tags: [], threat: [], + threshold: undefined, timestampOverride: undefined, to: 'now', type: 'machine_learning', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts index fd9e87e65d10d..ad4038b05dbd3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts @@ -42,6 +42,7 @@ export const createRules = async ({ severityMapping, tags, threat, + threshold, timestampOverride, to, type, @@ -84,6 +85,7 @@ export const createRules = async ({ severity, severityMapping, threat, + threshold, timestampOverride, to, type, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts index 8a86a0f103371..3af0c3f55b485 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts @@ -47,6 +47,7 @@ export const installPrepackagedRules = ( to, type, threat, + threshold, timestamp_override: timestampOverride, references, note, @@ -92,6 +93,7 @@ export const installPrepackagedRules = ( to, type, threat, + threshold, timestampOverride, references, note, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts index f3102a5ad2cf3..cfb40056eb85d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts @@ -143,6 +143,7 @@ export const getPatchRulesOptionsMock = (): PatchRulesOptions => ({ severityMapping: [], tags: [], threat: [], + threshold: undefined, timestampOverride: undefined, to: 'now', type: 'query', @@ -185,6 +186,7 @@ export const getPatchMlRulesOptionsMock = (): PatchRulesOptions => ({ severityMapping: [], tags: [], threat: [], + threshold: undefined, timestampOverride: undefined, to: 'now', type: 'machine_learning', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts index 577d8d426b63d..e0814647b4c39 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts @@ -42,6 +42,7 @@ export const patchRules = async ({ severityMapping, tags, threat, + threshold, timestampOverride, to, type, @@ -83,6 +84,7 @@ export const patchRules = async ({ severityMapping, tags, threat, + threshold, timestampOverride, to, type, @@ -121,6 +123,7 @@ export const patchRules = async ({ severity, severityMapping, threat, + threshold, timestampOverride, to, type, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index 7b793ffbdb362..b845990fd94ef 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -59,6 +59,7 @@ import { TagsOrUndefined, ToOrUndefined, ThreatOrUndefined, + ThresholdOrUndefined, TypeOrUndefined, ReferencesOrUndefined, PerPageOrUndefined, @@ -204,6 +205,7 @@ export interface CreateRulesOptions { severityMapping: SeverityMapping; tags: Tags; threat: Threat; + threshold: ThresholdOrUndefined; timestampOverride: TimestampOverrideOrUndefined; to: To; type: Type; @@ -247,6 +249,7 @@ export interface UpdateRulesOptions { severityMapping: SeverityMapping; tags: Tags; threat: Threat; + threshold: ThresholdOrUndefined; timestampOverride: TimestampOverrideOrUndefined; to: To; type: Type; @@ -288,6 +291,7 @@ export interface PatchRulesOptions { severityMapping: SeverityMappingOrUndefined; tags: TagsOrUndefined; threat: ThreatOrUndefined; + threshold: ThresholdOrUndefined; timestampOverride: TimestampOverrideOrUndefined; to: ToOrUndefined; type: TypeOrUndefined; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts index 6466cc596d891..bf97784e8d917 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts @@ -45,6 +45,7 @@ export const updatePrepackagedRules = async ( to, type, threat, + threshold, timestamp_override: timestampOverride, references, version, @@ -93,6 +94,7 @@ export const updatePrepackagedRules = async ( to, type, threat, + threshold, references, version, note, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts index fdc0a61274e75..650b59fb85bc0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts @@ -41,6 +41,7 @@ export const getUpdateRulesOptionsMock = (): UpdateRulesOptions => ({ severityMapping: [], tags: [], threat: [], + threshold: undefined, timestampOverride: undefined, to: 'now', type: 'query', @@ -84,6 +85,7 @@ export const getUpdateMlRulesOptionsMock = (): UpdateRulesOptions => ({ severityMapping: [], tags: [], threat: [], + threshold: undefined, timestampOverride: undefined, to: 'now', type: 'machine_learning', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts index 669b70aca4c9d..494a4e221d862 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts @@ -43,6 +43,7 @@ export const updateRules = async ({ severityMapping, tags, threat, + threshold, timestampOverride, to, type, @@ -85,6 +86,7 @@ export const updateRules = async ({ severityMapping, tags, threat, + threshold, timestampOverride, to, type, @@ -129,6 +131,7 @@ export const updateRules = async ({ severity, severityMapping, threat, + threshold, timestampOverride, to, type, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts index aa0512678b073..17505a4478261 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts @@ -53,6 +53,7 @@ describe('utils', () => { severityMapping: undefined, tags: undefined, threat: undefined, + threshold: undefined, to: undefined, timestampOverride: undefined, type: undefined, @@ -94,6 +95,7 @@ describe('utils', () => { severityMapping: undefined, tags: undefined, threat: undefined, + threshold: undefined, to: undefined, timestampOverride: undefined, type: undefined, @@ -135,6 +137,7 @@ describe('utils', () => { severityMapping: undefined, tags: undefined, threat: undefined, + threshold: undefined, to: undefined, timestampOverride: undefined, type: undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts index 861d02a8203e6..49c02f92ff336 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts @@ -29,6 +29,7 @@ import { TagsOrUndefined, ToOrUndefined, ThreatOrUndefined, + ThresholdOrUndefined, TypeOrUndefined, ReferencesOrUndefined, AuthorOrUndefined, @@ -82,6 +83,7 @@ export interface UpdateProperties { severityMapping: SeverityMappingOrUndefined; tags: TagsOrUndefined; threat: ThreatOrUndefined; + threshold: ThresholdOrUndefined; timestampOverride: TimestampOverrideOrUndefined; to: ToOrUndefined; type: TypeOrUndefined; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 7492422968062..17e05109b9a87 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -46,6 +46,7 @@ export const sampleRuleAlertParams = ( machineLearningJobId: undefined, filters: undefined, savedId: undefined, + threshold: undefined, timelineId: undefined, timelineTitle: undefined, timestampOverride: undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts index b368c8fe36054..452ba958876d6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts @@ -291,4 +291,158 @@ describe('create_signals', () => { }, }); }); + test('if aggregations is not provided it should not be included', () => { + const query = buildEventsSearchQuery({ + index: ['auditbeat-*'], + from: 'now-5m', + to: 'today', + filter: {}, + size: 100, + searchAfterSortId: undefined, + }); + expect(query).toEqual({ + allowNoIndices: true, + index: ['auditbeat-*'], + size: 100, + ignoreUnavailable: true, + body: { + query: { + bool: { + filter: [ + {}, + { + bool: { + filter: [ + { + bool: { + should: [ + { + range: { + '@timestamp': { + gte: 'now-5m', + }, + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + range: { + '@timestamp': { + lte: 'today', + }, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + { + match_all: {}, + }, + ], + }, + }, + + sort: [ + { + '@timestamp': { + order: 'asc', + }, + }, + ], + }, + }); + }); + + test('if aggregations is provided it should be included', () => { + const query = buildEventsSearchQuery({ + aggregations: { + tags: { + terms: { + field: 'tag', + }, + }, + }, + index: ['auditbeat-*'], + from: 'now-5m', + to: 'today', + filter: {}, + size: 100, + searchAfterSortId: undefined, + }); + expect(query).toEqual({ + allowNoIndices: true, + index: ['auditbeat-*'], + size: 100, + ignoreUnavailable: true, + body: { + query: { + bool: { + filter: [ + {}, + { + bool: { + filter: [ + { + bool: { + should: [ + { + range: { + '@timestamp': { + gte: 'now-5m', + }, + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + range: { + '@timestamp': { + lte: 'today', + }, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + { + match_all: {}, + }, + ], + }, + }, + aggregations: { + tags: { + terms: { + field: 'tag', + }, + }, + }, + sort: [ + { + '@timestamp': { + order: 'asc', + }, + }, + ], + }, + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts index c75dddf896fd1..dcf3a90364a40 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts @@ -5,6 +5,7 @@ */ interface BuildEventsSearchQuery { + aggregations?: unknown; index: string[]; from: string; to: string; @@ -14,6 +15,7 @@ interface BuildEventsSearchQuery { } export const buildEventsSearchQuery = ({ + aggregations, index, from, to, @@ -74,6 +76,7 @@ export const buildEventsSearchQuery = ({ ], }, }, + ...(aggregations ? { aggregations } : {}), sort: [ { '@timestamp': { @@ -83,6 +86,7 @@ export const buildEventsSearchQuery = ({ ], }, }; + if (searchAfterSortId) { return { ...searchQuery, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts index fc8b26450c852..9e118f77a73e7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts @@ -83,5 +83,6 @@ export const buildRule = ({ exceptions_list: ruleParams.exceptionsList ?? [], machine_learning_job_id: ruleParams.machineLearningJobId, anomaly_threshold: ruleParams.anomalyThreshold, + threshold: ruleParams.threshold, }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts index 77a63c63ff97a..e7098c015c165 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts @@ -46,7 +46,7 @@ export const buildSignal = (doc: SignalSourceHit, rule: Partial): S const ruleWithoutInternalTags = removeInternalTagsFromRule(rule); const parent = buildAncestor(doc, rule); const ancestors = buildAncestorsSignal(doc, rule); - const signal: Signal = { + let signal: Signal = { parent, ancestors, original_time: doc._source['@timestamp'], @@ -54,7 +54,11 @@ export const buildSignal = (doc: SignalSourceHit, rule: Partial): S rule: ruleWithoutInternalTags, }; if (doc._source.event != null) { - return { ...signal, original_event: doc._source.event }; + signal = { ...signal, original_event: doc._source.event }; + } + if (doc._source.threshold_count != null) { + signal = { ...signal, threshold_count: doc._source.threshold_count }; + delete doc._source.threshold_count; } return signal; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.test.ts new file mode 100644 index 0000000000000..744e2b0c06efe --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.test.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getThresholdSignalQueryFields } from './bulk_create_threshold_signals'; + +describe('getThresholdSignalQueryFields', () => { + it('should return proper fields for match_phrase filters', () => { + const mockFilters = { + bool: { + must: [], + filter: [ + { + bool: { + filter: [ + { + bool: { + should: [ + { + match_phrase: { + 'event.module': 'traefik', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + filter: [ + { + bool: { + should: [ + { + match_phrase: { + 'event.dataset': 'traefik.access', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + match_phrase: { + 'traefik.access.entryPointName': 'web-secure', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + ], + }, + }, + { + match_phrase: { + 'url.domain': 'kibana.siem.estc.dev', + }, + }, + ], + should: [], + must_not: [], + }, + }; + + expect(getThresholdSignalQueryFields(mockFilters)).toEqual({ + 'event.module': 'traefik', + 'event.dataset': 'traefik.access', + 'traefik.access.entryPointName': 'web-secure', + 'url.domain': 'kibana.siem.estc.dev', + }); + }); + + it('should return proper fields object for nested match filters', () => { + const filters = { + bool: { + must: [], + filter: [ + { + bool: { + filter: [ + { + bool: { + should: [ + { + match_phrase: { + 'event.module': 'traefik', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + match: { + 'event.dataset': 'traefik.access', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + ], + should: [], + must_not: [], + }, + }; + + expect(getThresholdSignalQueryFields(filters)).toEqual({ + 'event.module': 'traefik', + 'event.dataset': 'traefik.access', + }); + }); + + it('should return proper object for simple match filters', () => { + const filters = { + bool: { + must: [], + filter: [ + { + bool: { + should: [ + { + match: { + 'event.module': 'traefik', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + match_phrase: { + 'event.dataset': 'traefik.access', + }, + }, + ], + should: [], + must_not: [], + }, + }; + + expect(getThresholdSignalQueryFields(filters)).toEqual({ + 'event.module': 'traefik', + 'event.dataset': 'traefik.access', + }); + }); + + it('should return proper object for simple match_phrase filters', () => { + const filters = { + bool: { + must: [], + filter: [ + { + bool: { + should: [ + { + match_phrase: { + 'event.module': 'traefik', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + match_phrase: { + 'event.dataset': 'traefik.access', + }, + }, + ], + should: [], + must_not: [], + }, + }; + + expect(getThresholdSignalQueryFields(filters)).toEqual({ + 'event.module': 'traefik', + 'event.dataset': 'traefik.access', + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts new file mode 100644 index 0000000000000..ef9fbe485b92f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import uuidv5 from 'uuid/v5'; +import { reduce, get, isEmpty } from 'lodash/fp'; +import set from 'set-value'; + +import { Threshold } from '../../../../common/detection_engine/schemas/common/schemas'; +import { Logger } from '../../../../../../../src/core/server'; +import { AlertServices } from '../../../../../alerts/server'; +import { RuleAlertAction } from '../../../../common/detection_engine/types'; +import { RuleTypeParams, RefreshTypes } from '../types'; +import { singleBulkCreate, SingleBulkCreateResponse } from './single_bulk_create'; +import { SignalSearchResponse } from './types'; + +// used to generate constant Threshold Signals ID when run with the same params +const NAMESPACE_ID = '0684ec03-7201-4ee0-8ee0-3a3f6b2479b2'; + +interface BulkCreateThresholdSignalsParams { + actions: RuleAlertAction[]; + someResult: SignalSearchResponse; + ruleParams: RuleTypeParams; + services: AlertServices; + inputIndexPattern: string[]; + logger: Logger; + id: string; + filter: unknown; + signalsIndex: string; + name: string; + createdAt: string; + createdBy: string; + updatedAt: string; + updatedBy: string; + interval: string; + enabled: boolean; + refresh: RefreshTypes; + tags: string[]; + throttle: string; + startedAt: Date; +} + +interface FilterObject { + bool?: { + filter?: FilterObject | FilterObject[]; + should?: Array>>; + }; +} + +const getNestedQueryFilters = (filtersObj: FilterObject): Record => { + if (Array.isArray(filtersObj.bool?.filter)) { + return reduce( + (acc, filterItem) => { + const nestedFilter = getNestedQueryFilters(filterItem); + + if (nestedFilter) { + return { ...acc, ...nestedFilter }; + } + + return acc; + }, + {}, + filtersObj.bool?.filter + ); + } else { + return ( + (filtersObj.bool?.should && + filtersObj.bool?.should[0] && + (filtersObj.bool.should[0].match || filtersObj.bool.should[0].match_phrase)) ?? + {} + ); + } +}; + +export const getThresholdSignalQueryFields = (filter: unknown) => { + const filters = get('bool.filter', filter); + + return reduce( + (acc, item) => { + if (item.match_phrase) { + return { ...acc, ...item.match_phrase }; + } + + if (item.bool.should && (item.bool.should[0].match || item.bool.should[0].match_phrase)) { + return { ...acc, ...(item.bool.should[0].match || item.bool.should[0].match_phrase) }; + } + + if (item.bool?.filter) { + return { ...acc, ...getNestedQueryFilters(item) }; + } + + return acc; + }, + {}, + filters + ); +}; + +const getTransformedHits = ( + results: SignalSearchResponse, + inputIndex: string, + startedAt: Date, + threshold: Threshold, + ruleId: string, + signalQueryFields: Record +) => { + if (isEmpty(threshold.field)) { + const totalResults = + typeof results.hits.total === 'number' ? results.hits.total : results.hits.total.value; + + if (totalResults < threshold.value) { + return []; + } + + const source = { + '@timestamp': new Date().toISOString(), + threshold_count: totalResults, + ...signalQueryFields, + }; + + return [ + { + _index: inputIndex, + _id: uuidv5(`${ruleId}${startedAt}${threshold.field}`, NAMESPACE_ID), + _source: source, + }, + ]; + } + + if (!results.aggregations?.threshold) { + return []; + } + + return results.aggregations.threshold.buckets.map( + ({ key, doc_count }: { key: string; doc_count: number }) => { + const source = { + '@timestamp': new Date().toISOString(), + threshold_count: doc_count, + ...signalQueryFields, + }; + + set(source, threshold.field, key); + + return { + _index: inputIndex, + _id: uuidv5(`${ruleId}${startedAt}${threshold.field}${key}`, NAMESPACE_ID), + _source: source, + }; + } + ); +}; + +export const transformThresholdResultsToEcs = ( + results: SignalSearchResponse, + inputIndex: string, + startedAt: Date, + filter: unknown, + threshold: Threshold, + ruleId: string +): SignalSearchResponse => { + const signalQueryFields = getThresholdSignalQueryFields(filter); + const transformedHits = getTransformedHits( + results, + inputIndex, + startedAt, + threshold, + ruleId, + signalQueryFields + ); + const thresholdResults = { + ...results, + hits: { + ...results.hits, + hits: transformedHits, + }, + }; + + set(thresholdResults, 'results.hits.total', transformedHits.length); + + return thresholdResults; +}; + +export const bulkCreateThresholdSignals = async ( + params: BulkCreateThresholdSignalsParams +): Promise => { + const thresholdResults = params.someResult; + const ecsResults = transformThresholdResultsToEcs( + thresholdResults, + params.inputIndexPattern.join(','), + params.startedAt, + params.filter, + params.ruleParams.threshold!, + params.ruleParams.ruleId + ); + + return singleBulkCreate({ ...params, filteredEvents: ecsResults }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts new file mode 100644 index 0000000000000..a9a199f210da0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash/fp'; + +import { Threshold } from '../../../../common/detection_engine/schemas/common/schemas'; +import { singleSearchAfter } from './single_search_after'; + +import { AlertServices } from '../../../../../alerts/server'; +import { Logger } from '../../../../../../../src/core/server'; +import { SignalSearchResponse } from './types'; + +interface FindThresholdSignalsParams { + from: string; + to: string; + inputIndexPattern: string[]; + services: AlertServices; + logger: Logger; + filter: unknown; + threshold: Threshold; +} + +export const findThresholdSignals = async ({ + from, + to, + inputIndexPattern, + services, + logger, + filter, + threshold, +}: FindThresholdSignalsParams): Promise<{ + searchResult: SignalSearchResponse; + searchDuration: string; +}> => { + const aggregations = + threshold && !isEmpty(threshold.field) + ? { + threshold: { + terms: { + field: threshold.field, + min_doc_count: threshold.value, + }, + }, + } + : {}; + + return singleSearchAfter({ + aggregations, + searchAfterSortId: undefined, + index: inputIndexPattern, + from, + to, + services, + logger, + filter, + pageSize: 0, + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts index 4bd9de734f448..67dc1d50eefcd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts @@ -49,49 +49,62 @@ export const getFilter = async ({ query, lists, }: GetFilterArgs): Promise => { + const queryFilter = () => { + if (query != null && language != null && index != null) { + return getQueryFilter(query, language, filters || [], index, lists); + } else { + throw new BadRequestError('query, filters, and index parameter should be defined'); + } + }; + + const savedQueryFilter = async () => { + if (savedId != null && index != null) { + try { + // try to get the saved object first + const savedObject = await services.savedObjectsClient.get( + 'query', + savedId + ); + return getQueryFilter( + savedObject.attributes.query.query, + savedObject.attributes.query.language, + savedObject.attributes.filters, + index, + lists + ); + } catch (err) { + // saved object does not exist, so try and fall back if the user pushed + // any additional language, query, filters, etc... + if (query != null && language != null && index != null) { + return getQueryFilter(query, language, filters || [], index, lists); + } else { + // user did not give any additional fall back mechanism for generating a rule + // rethrow error for activity monitoring + throw err; + } + } + } else { + throw new BadRequestError('savedId parameter should be defined'); + } + }; + switch (type) { + case 'threshold': { + return savedId != null ? savedQueryFilter() : queryFilter(); + } case 'query': { - if (query != null && language != null && index != null) { - return getQueryFilter(query, language, filters || [], index, lists); - } else { - throw new BadRequestError('query, filters, and index parameter should be defined'); - } + return queryFilter(); } case 'saved_query': { - if (savedId != null && index != null) { - try { - // try to get the saved object first - const savedObject = await services.savedObjectsClient.get( - 'query', - savedId - ); - return getQueryFilter( - savedObject.attributes.query.query, - savedObject.attributes.query.language, - savedObject.attributes.filters, - index, - lists - ); - } catch (err) { - // saved object does not exist, so try and fall back if the user pushed - // any additional language, query, filters, etc... - if (query != null && language != null && index != null) { - return getQueryFilter(query, language, filters || [], index, lists); - } else { - // user did not give any additional fall back mechanism for generating a rule - // rethrow error for activity monitoring - throw err; - } - } - } else { - throw new BadRequestError('savedId parameter should be defined'); - } + return savedQueryFilter(); } case 'machine_learning': { throw new BadRequestError( 'Unsupported Rule of type "machine_learning" supplied to getFilter' ); } + default: { + return assertUnreachable(type); + } } - return assertUnreachable(type); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts index 2583cf2c8da91..d08ca90f3e353 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts @@ -37,6 +37,9 @@ const signalSchema = schema.object({ severity: schema.string(), severityMapping: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), threat: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + threshold: schema.maybe( + schema.object({ field: schema.nullable(schema.string()), value: schema.number() }) + ), timestampOverride: schema.nullable(schema.string()), to: schema.string(), type: schema.string(), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 49945134e378b..49efc30b9704d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -26,7 +26,9 @@ import { getGapBetweenRuns, parseScheduleDates, getListsClient, getExceptions } import { signalParamsSchema } from './signal_params_schema'; import { siemRuleActionGroups } from './siem_rule_action_groups'; import { findMlSignals } from './find_ml_signals'; +import { findThresholdSignals } from './find_threshold_signals'; import { bulkCreateMlSignals } from './bulk_create_ml_signals'; +import { bulkCreateThresholdSignals } from './bulk_create_threshold_signals'; import { scheduleNotificationActions, NotificationRuleTypeParams, @@ -58,6 +60,7 @@ export const signalRulesAlertType = ({ producer: SERVER_APP_ID, async executor({ previousStartedAt, + startedAt, alertId, services, params, @@ -78,6 +81,7 @@ export const signalRulesAlertType = ({ savedId, query, to, + threshold, type, exceptionsList, } = params; @@ -224,6 +228,60 @@ export const signalRulesAlertType = ({ if (bulkCreateDuration) { result.bulkCreateTimes.push(bulkCreateDuration); } + } else if (type === 'threshold' && threshold) { + const inputIndex = await getInputIndex(services, version, index); + const esFilter = await getFilter({ + type, + filters, + language, + query, + savedId, + services, + index: inputIndex, + lists: exceptionItems ?? [], + }); + + const { searchResult: thresholdResults } = await findThresholdSignals({ + inputIndexPattern: inputIndex, + from, + to, + services, + logger, + filter: esFilter, + threshold, + }); + + const { + success, + bulkCreateDuration, + createdItemsCount, + } = await bulkCreateThresholdSignals({ + actions, + throttle, + someResult: thresholdResults, + ruleParams: params, + filter: esFilter, + services, + logger, + id: alertId, + inputIndexPattern: inputIndex, + signalsIndex: outputIndex, + startedAt, + name, + createdBy, + createdAt, + updatedBy, + updatedAt, + interval, + enabled, + refresh, + tags, + }); + result.success = success; + result.createdSignalsCount = createdItemsCount; + if (bulkCreateDuration) { + result.bulkCreateTimes.push(bulkCreateDuration); + } } else { const inputIndex = await getInputIndex(services, version, index); const esFilter = await getFilter({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts index 409f374d7df1e..daea277f14368 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts @@ -12,6 +12,7 @@ import { buildEventsSearchQuery } from './build_events_query'; import { makeFloatString } from './utils'; interface SingleSearchAfterParams { + aggregations?: unknown; searchAfterSortId: string | undefined; index: string[]; from: string; @@ -24,6 +25,7 @@ interface SingleSearchAfterParams { // utilize search_after for paging results into bulk. export const singleSearchAfter = async ({ + aggregations, searchAfterSortId, index, from, @@ -38,6 +40,7 @@ export const singleSearchAfter = async ({ }> => { try { const searchAfterQuery = buildEventsSearchQuery({ + aggregations, index, from, to, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 082211df28320..5d6bafc5a6d09 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -121,6 +121,7 @@ export interface Signal { original_time: string; original_event?: SearchTypes; status: Status; + threshold_count?: SearchTypes; } export interface SignalHit { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts index 365222d62d322..4b4f5147c9a42 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts @@ -9,6 +9,7 @@ import { Description, NoteOrUndefined, ThreatOrUndefined, + ThresholdOrUndefined, FalsePositives, From, Immutable, @@ -71,6 +72,7 @@ export interface RuleTypeParams { severity: Severity; severityMapping: SeverityMappingOrUndefined; threat: ThreatOrUndefined; + threshold: ThresholdOrUndefined; timestampOverride: TimestampOverrideOrUndefined; to: To; type: RuleType; diff --git a/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts b/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts index 17ed6d20db29e..19b16bd4bc6d2 100644 --- a/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts +++ b/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts @@ -322,6 +322,7 @@ export const signalFieldsMap: Readonly> = { 'signal.rule.updated_by': 'signal.rule.updated_by', 'signal.rule.version': 'signal.rule.version', 'signal.rule.note': 'signal.rule.note', + 'signal.rule.threshold': 'signal.rule.threshold', 'signal.rule.exceptions_list': 'signal.rule.exceptions_list', }; From 8763652a7bbe5ff49484fd9b4ede359a75f2a63c Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Tue, 14 Jul 2020 12:57:00 -0400 Subject: [PATCH 15/82] [Resolver] Remove Client side API limits (#71660) --- .../public/resolver/store/middleware/resolver_tree_fetcher.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts index 59e944d95e04b..7d16dc251e6fc 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts @@ -70,10 +70,6 @@ export function ResolverTreeFetcher( const entityIDToFetch = matchingEntities[0].entity_id; result = await context.services.http.get(`/api/endpoint/resolver/${entityIDToFetch}`, { signal: lastRequestAbortController.signal, - query: { - children: 5, - ancestors: 5, - }, }); } catch (error) { // https://developer.mozilla.org/en-US/docs/Web/API/DOMException#exception-AbortError From 7243e97883bb79895e2f5432ce8529291725cca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 14 Jul 2020 17:59:46 +0100 Subject: [PATCH 16/82] [Observability] Landing page: Add promo panel for Ingest Manager (#71213) * adding ingest manager panel * adding ingest manager panel * changing url * removing git conflicts --- .../public/application/index.tsx | 4 +- .../app/ingest_manager_panel/index.tsx | 52 +++++++++++++++++++ ...se_url_params.tsx => use_route_params.tsx} | 2 +- .../public/pages/landing/index.tsx | 11 ++++ .../services/get_observability_alerts.test.ts | 5 ++ .../services/get_observability_alerts.ts | 15 +++--- 6 files changed, 80 insertions(+), 9 deletions(-) create mode 100644 x-pack/plugins/observability/public/components/app/ingest_manager_panel/index.tsx rename x-pack/plugins/observability/public/hooks/{use_url_params.tsx => use_route_params.tsx} (97%) diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index 5bc8d96656ed4..8cfbca37e8d05 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -12,7 +12,7 @@ import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public import { AppMountParameters, CoreStart } from '../../../../../src/core/public'; import { EuiThemeProvider } from '../../../../legacy/common/eui_styled_components'; import { PluginContext } from '../context/plugin_context'; -import { useUrlParams } from '../hooks/use_url_params'; +import { useRouteParams } from '../hooks/use_route_params'; import { routes } from '../routes'; import { usePluginContext } from '../hooks/use_plugin_context'; @@ -36,7 +36,7 @@ const App = () => { ]); }, [core]); - const { query, path: pathParams } = useUrlParams(route.params); + const { query, path: pathParams } = useRouteParams(route.params); return route.handler({ query, path: pathParams }); }; return ; diff --git a/x-pack/plugins/observability/public/components/app/ingest_manager_panel/index.tsx b/x-pack/plugins/observability/public/components/app/ingest_manager_panel/index.tsx new file mode 100644 index 0000000000000..f7a1deb83fbe4 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/ingest_manager_panel/index.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { EuiPanel } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiText } from '@elastic/eui'; +import { EuiLink } from '@elastic/eui'; + +export const IngestManagerPanel = () => { + return ( + + + + +

+ {i18n.translate('xpack.observability.ingestManafer.title', { + defaultMessage: 'Have you seen our new Ingest Manager?', + })} +

+
+
+ + + {i18n.translate('xpack.observability.ingestManafer.text', { + defaultMessage: + 'The Elastic Agent provides a simple, unified way to add monitoring for logs, metrics, and other types of data to your hosts. You no longer need to install multiple Beats and other agents, making it easier and faster to deploy configurations across your infrastructure.', + })} + + + + + {i18n.translate('xpack.observability.ingestManafer.button', { + defaultMessage: 'Try Ingest Manager Beta', + })} + + +
+
+ ); +}; diff --git a/x-pack/plugins/observability/public/hooks/use_url_params.tsx b/x-pack/plugins/observability/public/hooks/use_route_params.tsx similarity index 97% rename from x-pack/plugins/observability/public/hooks/use_url_params.tsx rename to x-pack/plugins/observability/public/hooks/use_route_params.tsx index 680a32fb49677..93a79bfda7fc1 100644 --- a/x-pack/plugins/observability/public/hooks/use_url_params.tsx +++ b/x-pack/plugins/observability/public/hooks/use_route_params.tsx @@ -23,7 +23,7 @@ function getQueryParams(location: ReturnType) { * It removes any aditional item which is not declared in the type. * @param params */ -export function useUrlParams(params: Params) { +export function useRouteParams(params: Params) { const location = useLocation(); const pathParams = useParams(); const queryParams = getQueryParams(location); diff --git a/x-pack/plugins/observability/public/pages/landing/index.tsx b/x-pack/plugins/observability/public/pages/landing/index.tsx index 512f4428d9bf2..da46791d9e855 100644 --- a/x-pack/plugins/observability/public/pages/landing/index.tsx +++ b/x-pack/plugins/observability/public/pages/landing/index.tsx @@ -22,6 +22,7 @@ import styled, { ThemeContext } from 'styled-components'; import { WithHeaderLayout } from '../../components/app/layout/with_header'; import { usePluginContext } from '../../hooks/use_plugin_context'; import { appsSection } from '../home/section'; +import { IngestManagerPanel } from '../../components/app/ingest_manager_panel'; const EuiCardWithoutPadding = styled(EuiCard)` padding: 0; @@ -112,6 +113,16 @@ export const LandingPage = () => {
+ + + + + + + + + + ); diff --git a/x-pack/plugins/observability/public/services/get_observability_alerts.test.ts b/x-pack/plugins/observability/public/services/get_observability_alerts.test.ts index dd3f476fe7d53..36ef1241983a5 100644 --- a/x-pack/plugins/observability/public/services/get_observability_alerts.test.ts +++ b/x-pack/plugins/observability/public/services/get_observability_alerts.test.ts @@ -7,6 +7,8 @@ import { AppMountContext } from 'kibana/public'; import { getObservabilityAlerts } from './get_observability_alerts'; +const basePath = { prepend: (path: string) => path }; + describe('getObservabilityAlerts', () => { it('Returns empty array when api throws exception', async () => { const core = ({ @@ -14,6 +16,7 @@ describe('getObservabilityAlerts', () => { get: async () => { throw new Error('Boom'); }, + basePath, }, } as unknown) as AppMountContext['core']; @@ -29,6 +32,7 @@ describe('getObservabilityAlerts', () => { data: undefined, }; }, + basePath, }, } as unknown) as AppMountContext['core']; @@ -65,6 +69,7 @@ describe('getObservabilityAlerts', () => { ], }; }, + basePath, }, } as unknown) as AppMountContext['core']; diff --git a/x-pack/plugins/observability/public/services/get_observability_alerts.ts b/x-pack/plugins/observability/public/services/get_observability_alerts.ts index 49855a30c16f6..58ff9c92acbff 100644 --- a/x-pack/plugins/observability/public/services/get_observability_alerts.ts +++ b/x-pack/plugins/observability/public/services/get_observability_alerts.ts @@ -9,12 +9,15 @@ import { Alert } from '../../../alerts/common'; export async function getObservabilityAlerts({ core }: { core: AppMountContext['core'] }) { try { - const { data = [] }: { data: Alert[] } = await core.http.get('/api/alerts/_find', { - query: { - page: 1, - per_page: 20, - }, - }); + const { data = [] }: { data: Alert[] } = await core.http.get( + core.http.basePath.prepend('/api/alerts/_find'), + { + query: { + page: 1, + per_page: 20, + }, + } + ); return data.filter(({ consumer }) => { return ( From ef2a5839810ebff3b07e87052d1d9fc9f4856b0f Mon Sep 17 00:00:00 2001 From: Sonja Krause-Harder Date: Tue, 14 Jul 2020 19:03:28 +0200 Subject: [PATCH 17/82] [Ingest Manager] Send kibana version on requests to package registry (#71443) * Send kibana version on requests to registry search. * Only use semver part of kibanaVersion. * Adjust test. Co-authored-by: Elastic Machine --- .../ingest_manager/server/services/epm/registry/index.ts | 9 +++++++++ .../test/ingest_manager_api_integration/apis/epm/list.ts | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts index ea906517f6dec..7fb13e5e671d0 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts @@ -21,6 +21,7 @@ import { ArchiveEntry, untarBuffer } from './extract'; import { fetchUrl, getResponse, getResponseStream } from './requests'; import { streamToBuffer } from './streams'; import { getRegistryUrl } from './registry_url'; +import { appContextService } from '../..'; export { ArchiveEntry } from './extract'; @@ -47,6 +48,10 @@ export async function fetchList(params?: SearchParams): Promise Date: Tue, 14 Jul 2020 13:13:20 -0400 Subject: [PATCH 18/82] [Security Solution][Lists] - Update exception comments logic in API (#71602) ### Summary Updated the logic so that newly added exception item comments are shown as expected. --- .../services/exception_lists/utils.test.ts | 129 +++++++++++------- .../server/services/exception_lists/utils.ts | 13 +- 2 files changed, 85 insertions(+), 57 deletions(-) diff --git a/x-pack/plugins/lists/server/services/exception_lists/utils.test.ts b/x-pack/plugins/lists/server/services/exception_lists/utils.test.ts index 9cc2aacd88458..6f0c5195f2025 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/utils.test.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/utils.test.ts @@ -6,7 +6,7 @@ import sinon from 'sinon'; import moment from 'moment'; -import { DATE_NOW, USER } from '../../../common/constants.mock'; +import { USER } from '../../../common/constants.mock'; import { isCommentEqual, @@ -16,8 +16,9 @@ import { } from './utils'; describe('utils', () => { - const anchor = '2020-06-17T20:34:51.337Z'; - const unix = moment(anchor).valueOf(); + const oldDate = '2020-03-17T20:34:51.337Z'; + const dateNow = '2020-06-17T20:34:51.337Z'; + const unix = moment(dateNow).valueOf(); let clock: sinon.SinonFakeTimers; beforeEach(() => { @@ -42,11 +43,11 @@ describe('utils', () => { test('it formats newly added comments', () => { const comments = transformUpdateCommentsToComments({ comments: [ - { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + { comment: 'Im an old comment', created_at: oldDate, created_by: 'bane' }, { comment: 'Im a new comment' }, ], existingComments: [ - { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + { comment: 'Im an old comment', created_at: oldDate, created_by: 'bane' }, ], user: 'lily', }); @@ -54,12 +55,12 @@ describe('utils', () => { expect(comments).toEqual([ { comment: 'Im an old comment', - created_at: anchor, - created_by: 'lily', + created_at: oldDate, + created_by: 'bane', }, { comment: 'Im a new comment', - created_at: anchor, + created_at: dateNow, created_by: 'lily', }, ]); @@ -68,12 +69,12 @@ describe('utils', () => { test('it formats multiple newly added comments', () => { const comments = transformUpdateCommentsToComments({ comments: [ - { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, { comment: 'Im a new comment' }, { comment: 'Im another new comment' }, ], existingComments: [ - { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, ], user: 'lily', }); @@ -81,17 +82,17 @@ describe('utils', () => { expect(comments).toEqual([ { comment: 'Im an old comment', - created_at: anchor, + created_at: oldDate, created_by: 'lily', }, { comment: 'Im a new comment', - created_at: anchor, + created_at: dateNow, created_by: 'lily', }, { comment: 'Im another new comment', - created_at: anchor, + created_at: dateNow, created_by: 'lily', }, ]); @@ -99,9 +100,9 @@ describe('utils', () => { test('it should not throw if comments match existing comments', () => { const comments = transformUpdateCommentsToComments({ - comments: [{ comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }], + comments: [{ comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }], existingComments: [ - { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, ], user: 'lily', }); @@ -109,7 +110,7 @@ describe('utils', () => { expect(comments).toEqual([ { comment: 'Im an old comment', - created_at: anchor, + created_at: oldDate, created_by: 'lily', }, ]); @@ -120,12 +121,12 @@ describe('utils', () => { comments: [ { comment: 'Im an old comment that is trying to be updated', - created_at: DATE_NOW, + created_at: oldDate, created_by: 'lily', }, ], existingComments: [ - { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, ], user: 'lily', }); @@ -133,9 +134,9 @@ describe('utils', () => { expect(comments).toEqual([ { comment: 'Im an old comment that is trying to be updated', - created_at: DATE_NOW, + created_at: oldDate, created_by: 'lily', - updated_at: anchor, + updated_at: dateNow, updated_by: 'lily', }, ]); @@ -150,7 +151,7 @@ describe('utils', () => { }, ], existingComments: [ - { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, ], user: 'lily', }) @@ -164,7 +165,7 @@ describe('utils', () => { transformUpdateCommentsToComments({ comments: [], existingComments: [ - { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, ], user: 'lily', }) @@ -176,9 +177,9 @@ describe('utils', () => { test('it throws if user tries to update existing comment timestamp', () => { expect(() => transformUpdateCommentsToComments({ - comments: [{ comment: 'Im an old comment', created_at: anchor, created_by: 'lily' }], + comments: [{ comment: 'Im an old comment', created_at: dateNow, created_by: 'lily' }], existingComments: [ - { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, ], user: 'bane', }) @@ -188,9 +189,9 @@ describe('utils', () => { test('it throws if user tries to update existing comment author', () => { expect(() => transformUpdateCommentsToComments({ - comments: [{ comment: 'Im an old comment', created_at: anchor, created_by: 'lily' }], + comments: [{ comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }], existingComments: [ - { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'me!' }, + { comment: 'Im an old comment', created_at: oldDate, created_by: 'me!' }, ], user: 'bane', }) @@ -203,12 +204,12 @@ describe('utils', () => { comments: [ { comment: 'Im an old comment that is trying to be updated', - created_at: DATE_NOW, + created_at: oldDate, created_by: 'lily', }, ], existingComments: [ - { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, ], user: 'bane', }) @@ -220,10 +221,10 @@ describe('utils', () => { transformUpdateCommentsToComments({ comments: [ { comment: 'Im a new comment' }, - { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, ], existingComments: [ - { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, ], user: 'lily', }) @@ -236,7 +237,7 @@ describe('utils', () => { expect(() => transformUpdateCommentsToComments({ comments: [ - { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, { comment: 'Im a new comment' }, ], existingComments: [], @@ -249,11 +250,11 @@ describe('utils', () => { expect(() => transformUpdateCommentsToComments({ comments: [ - { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, { comment: ' ' }, ], existingComments: [ - { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }, + { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, ], user: 'lily', }) @@ -280,12 +281,12 @@ describe('utils', () => { expect(comments).toEqual([ { comment: 'Im a new comment', - created_at: anchor, + created_at: dateNow, created_by: 'lily', }, { comment: 'Im another new comment', - created_at: anchor, + created_at: dateNow, created_by: 'lily', }, ]); @@ -302,16 +303,16 @@ describe('utils', () => { }); describe('#transformUpdateComments', () => { - test('it updates comment and adds "updated_at" and "updated_by"', () => { + test('it updates comment and adds "updated_at" and "updated_by" if content differs', () => { const comments = transformUpdateComments({ comment: { comment: 'Im an old comment that is trying to be updated', - created_at: DATE_NOW, + created_at: oldDate, created_by: 'lily', }, existingComment: { comment: 'Im an old comment', - created_at: DATE_NOW, + created_at: oldDate, created_by: 'lily', }, user: 'lily', @@ -319,24 +320,46 @@ describe('utils', () => { expect(comments).toEqual({ comment: 'Im an old comment that is trying to be updated', - created_at: '2020-04-20T15:25:31.830Z', + created_at: oldDate, created_by: 'lily', - updated_at: anchor, + updated_at: dateNow, updated_by: 'lily', }); }); + test('it does not update comment and add "updated_at" and "updated_by" if content is the same', () => { + const comments = transformUpdateComments({ + comment: { + comment: 'Im an old comment ', + created_at: oldDate, + created_by: 'lily', + }, + existingComment: { + comment: 'Im an old comment', + created_at: oldDate, + created_by: 'lily', + }, + user: 'lily', + }); + + expect(comments).toEqual({ + comment: 'Im an old comment', + created_at: oldDate, + created_by: 'lily', + }); + }); + test('it throws if user tries to update an existing comment that is not their own', () => { expect(() => transformUpdateComments({ comment: { comment: 'Im an old comment that is trying to be updated', - created_at: DATE_NOW, + created_at: oldDate, created_by: 'lily', }, existingComment: { comment: 'Im an old comment', - created_at: DATE_NOW, + created_at: oldDate, created_by: 'lily', }, user: 'bane', @@ -348,13 +371,13 @@ describe('utils', () => { expect(() => transformUpdateComments({ comment: { - comment: 'Im an old comment that is trying to be updated', - created_at: anchor, + comment: 'Im an old comment', + created_at: dateNow, created_by: 'lily', }, existingComment: { comment: 'Im an old comment', - created_at: DATE_NOW, + created_at: oldDate, created_by: 'lily', }, user: 'lily', @@ -368,12 +391,12 @@ describe('utils', () => { const result = isCommentEqual( { comment: 'some old comment', - created_at: DATE_NOW, + created_at: oldDate, created_by: USER, }, { comment: 'some older comment', - created_at: DATE_NOW, + created_at: oldDate, created_by: USER, } ); @@ -385,12 +408,12 @@ describe('utils', () => { const result = isCommentEqual( { comment: 'some old comment', - created_at: DATE_NOW, + created_at: oldDate, created_by: USER, }, { comment: 'some old comment', - created_at: anchor, + created_at: dateNow, created_by: USER, } ); @@ -402,12 +425,12 @@ describe('utils', () => { const result = isCommentEqual( { comment: 'some old comment', - created_at: DATE_NOW, + created_at: oldDate, created_by: USER, }, { comment: 'some old comment', - created_at: DATE_NOW, + created_at: oldDate, created_by: 'lily', } ); @@ -419,11 +442,11 @@ describe('utils', () => { const result = isCommentEqual( { comment: 'some old comment', - created_at: DATE_NOW, + created_at: oldDate, created_by: USER, }, { - created_at: DATE_NOW, + created_at: oldDate, created_by: USER, // Disabling to assure that order doesn't matter // eslint-disable-next-line sort-keys diff --git a/x-pack/plugins/lists/server/services/exception_lists/utils.ts b/x-pack/plugins/lists/server/services/exception_lists/utils.ts index ad1e1a3439d7c..3ef2c337e80b6 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/utils.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/utils.ts @@ -316,13 +316,15 @@ export const transformUpdateCommentsToComments = ({ 'When trying to update a comment, "created_at" and "created_by" must be present', 403 ); - } else if (commentsSchema.is(c) && existingComment == null) { + } else if (existingComment == null && commentsSchema.is(c)) { throw new ErrorWithStatusCode('Only new comments may be added', 403); } else if ( commentsSchema.is(c) && existingComment != null && - !isCommentEqual(c, existingComment) + isCommentEqual(c, existingComment) ) { + return existingComment; + } else if (commentsSchema.is(c) && existingComment != null) { return transformUpdateComments({ comment: c, existingComment, user }); } else { return transformCreateCommentsToComments({ comments: [c], user }) ?? []; @@ -347,14 +349,17 @@ export const transformUpdateComments = ({ throw new ErrorWithStatusCode('Unable to update comment', 403); } else if (comment.comment.trim().length === 0) { throw new ErrorWithStatusCode('Empty comments not allowed', 403); - } else { + } else if (comment.comment.trim() !== existingComment.comment) { const dateNow = new Date().toISOString(); return { - ...comment, + ...existingComment, + comment: comment.comment, updated_at: dateNow, updated_by: user, }; + } else { + return existingComment; } }; From c24f180391ecdef261b9138e9331bb62f93289e5 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Tue, 14 Jul 2020 12:36:01 -0500 Subject: [PATCH 19/82] [ML] Anomaly Detection: Annotations enhancements (#70198) Co-authored-by: Elastic Machine --- .../ml/common/constants/annotations.ts | 3 + .../plugins/ml/common/constants/anomalies.ts | 2 + x-pack/plugins/ml/common/types/annotations.ts | 45 ++- x-pack/plugins/ml/common/types/anomalies.ts | 4 + .../annotation_description_list/index.tsx | 30 +- .../annotations/annotation_flyout/index.tsx | 89 +++++- .../annotations_table.test.js.snap | 47 +++- .../annotations_table/annotations_table.js | 260 +++++++++++++++--- .../explorer/actions/load_explorer_data.ts | 2 +- .../public/application/explorer/explorer.d.ts | 5 - .../public/application/explorer/explorer.js | 72 +++-- .../application/explorer/explorer_utils.js | 16 +- .../reducers/explorer_reducer/reducer.ts | 2 +- .../reducers/explorer_reducer/state.ts | 11 +- .../application/routing/routes/explorer.tsx | 1 - .../services/ml_api_service/annotations.ts | 14 +- .../timeseriesexplorer/timeseriesexplorer.js | 57 +++- .../get_focus_data.ts | 19 +- .../models/annotation_service/annotation.ts | 101 ++++++- .../get_partition_fields_values.ts | 6 +- .../routes/schemas/annotations_schema.ts | 27 ++ .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 23 files changed, 697 insertions(+), 120 deletions(-) diff --git a/x-pack/plugins/ml/common/constants/annotations.ts b/x-pack/plugins/ml/common/constants/annotations.ts index 936ff610361af..4929dfb28eb15 100644 --- a/x-pack/plugins/ml/common/constants/annotations.ts +++ b/x-pack/plugins/ml/common/constants/annotations.ts @@ -13,3 +13,6 @@ export const ANNOTATION_USER_UNKNOWN = ''; // UI enforced limit to the maximum number of characters that can be entered for an annotation. export const ANNOTATION_MAX_LENGTH_CHARS = 1000; + +export const ANNOTATION_EVENT_USER = 'user'; +export const ANNOTATION_EVENT_DELAYED_DATA = 'delayed_data'; diff --git a/x-pack/plugins/ml/common/constants/anomalies.ts b/x-pack/plugins/ml/common/constants/anomalies.ts index bbf3616c05880..d15033b738b0f 100644 --- a/x-pack/plugins/ml/common/constants/anomalies.ts +++ b/x-pack/plugins/ml/common/constants/anomalies.ts @@ -20,3 +20,5 @@ export enum ANOMALY_THRESHOLD { WARNING = 3, LOW = 0, } + +export const PARTITION_FIELDS = ['partition_field', 'over_field', 'by_field'] as const; diff --git a/x-pack/plugins/ml/common/types/annotations.ts b/x-pack/plugins/ml/common/types/annotations.ts index f2f6fe111f5cc..159a598f16bf5 100644 --- a/x-pack/plugins/ml/common/types/annotations.ts +++ b/x-pack/plugins/ml/common/types/annotations.ts @@ -58,8 +58,20 @@ // ] // } +import { PartitionFieldsType } from './anomalies'; import { ANNOTATION_TYPE } from '../constants/annotations'; +export type AnnotationFieldName = 'partition_field_name' | 'over_field_name' | 'by_field_name'; +export type AnnotationFieldValue = 'partition_field_value' | 'over_field_value' | 'by_field_value'; + +export function getAnnotationFieldName(fieldType: PartitionFieldsType): AnnotationFieldName { + return `${fieldType}_name` as AnnotationFieldName; +} + +export function getAnnotationFieldValue(fieldType: PartitionFieldsType): AnnotationFieldValue { + return `${fieldType}_value` as AnnotationFieldValue; +} + export interface Annotation { _id?: string; create_time?: number; @@ -73,8 +85,15 @@ export interface Annotation { annotation: string; job_id: string; type: ANNOTATION_TYPE.ANNOTATION | ANNOTATION_TYPE.COMMENT; + event?: string; + detector_index?: number; + partition_field_name?: string; + partition_field_value?: string; + over_field_name?: string; + over_field_value?: string; + by_field_name?: string; + by_field_value?: string; } - export function isAnnotation(arg: any): arg is Annotation { return ( arg.timestamp !== undefined && @@ -93,3 +112,27 @@ export function isAnnotations(arg: any): arg is Annotations { } return arg.every((d: Annotation) => isAnnotation(d)); } + +export interface FieldToBucket { + field: string; + missing?: string | number; +} + +export interface FieldToBucketResult { + key: string; + doc_count: number; +} + +export interface TermAggregationResult { + doc_count_error_upper_bound: number; + sum_other_doc_count: number; + buckets: FieldToBucketResult[]; +} + +export type EsAggregationResult = Record; + +export interface GetAnnotationsResponse { + aggregations?: EsAggregationResult; + annotations: Record; + success: boolean; +} diff --git a/x-pack/plugins/ml/common/types/anomalies.ts b/x-pack/plugins/ml/common/types/anomalies.ts index 639d9b3b25fae..a23886e8fcdc6 100644 --- a/x-pack/plugins/ml/common/types/anomalies.ts +++ b/x-pack/plugins/ml/common/types/anomalies.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { PARTITION_FIELDS } from '../constants/anomalies'; + export interface Influencer { influencer_field_name: string; influencer_field_values: string[]; @@ -53,3 +55,5 @@ export interface AnomaliesTableRecord { typicalSort?: any; metricDescriptionSort?: number; } + +export type PartitionFieldsType = typeof PARTITION_FIELDS[number]; diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotation_description_list/index.tsx b/x-pack/plugins/ml/public/application/components/annotations/annotation_description_list/index.tsx index cf8fd299c07d7..eee2f8dca244d 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotation_description_list/index.tsx +++ b/x-pack/plugins/ml/public/application/components/annotations/annotation_description_list/index.tsx @@ -19,9 +19,10 @@ import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils'; interface Props { annotation: Annotation; + detectorDescription?: string; } -export const AnnotationDescriptionList = ({ annotation }: Props) => { +export const AnnotationDescriptionList = ({ annotation, detectorDescription }: Props) => { const listItems = [ { title: i18n.translate('xpack.ml.timeSeriesExplorer.annotationDescriptionList.jobIdTitle', { @@ -81,6 +82,33 @@ export const AnnotationDescriptionList = ({ annotation }: Props) => { description: annotation.modified_username, }); } + if (detectorDescription !== undefined) { + listItems.push({ + title: i18n.translate('xpack.ml.timeSeriesExplorer.annotationDescriptionList.detectorTitle', { + defaultMessage: 'Detector', + }), + description: detectorDescription, + }); + } + + if (annotation.partition_field_name !== undefined) { + listItems.push({ + title: annotation.partition_field_name, + description: annotation.partition_field_value, + }); + } + if (annotation.over_field_name !== undefined) { + listItems.push({ + title: annotation.over_field_name, + description: annotation.over_field_value, + }); + } + if (annotation.by_field_name !== undefined) { + listItems.push({ + title: annotation.by_field_name, + description: annotation.by_field_value, + }); + } return ( { public state: State = { isDeleteModalVisible: false, + applyAnnotationToSeries: true, }; public annotationSub: Rx.Subscription | null = null; @@ -150,11 +178,31 @@ class AnnotationFlyoutUI extends Component { }; public saveOrUpdateAnnotation = () => { - const { annotation } = this.props; - - if (annotation === null) { + const { annotation: originalAnnotation, chartDetails, detectorIndex } = this.props; + if (originalAnnotation === null) { return; } + const annotation = cloneDeep(originalAnnotation); + + if (this.state.applyAnnotationToSeries && chartDetails?.entityData?.entities) { + chartDetails.entityData.entities.forEach((entity: Entity) => { + const { fieldName, fieldValue } = entity; + const fieldType = entity.fieldType as PartitionFieldsType; + annotation[getAnnotationFieldName(fieldType)] = fieldName; + annotation[getAnnotationFieldValue(fieldType)] = fieldValue; + }); + annotation.detector_index = detectorIndex; + } + // if unchecked, remove all the partitions before indexing + if (!this.state.applyAnnotationToSeries) { + delete annotation.detector_index; + PARTITION_FIELDS.forEach((fieldType) => { + delete annotation[getAnnotationFieldName(fieldType)]; + delete annotation[getAnnotationFieldValue(fieldType)]; + }); + } + // Mark the annotation created by `user` if and only if annotation is being created, not updated + annotation.event = annotation.event ?? ANNOTATION_EVENT_USER; annotation$.next(null); @@ -214,7 +262,7 @@ class AnnotationFlyoutUI extends Component { }; public render(): ReactNode { - const { annotation } = this.props; + const { annotation, detectors, detectorIndex } = this.props; const { isDeleteModalVisible } = this.state; if (annotation === null) { @@ -242,10 +290,13 @@ class AnnotationFlyoutUI extends Component { } ); } + const detector = detectors ? detectors.find((d) => d.index === detectorIndex) : undefined; + const detectorDescription = + detector && 'detector_description' in detector ? detector.detector_description : ''; return ( - +

@@ -264,7 +315,10 @@ class AnnotationFlyoutUI extends Component { - + { value={annotation.annotation} /> + + + } + checked={this.state.applyAnnotationToSeries} + onChange={() => + this.setState({ + applyAnnotationToSeries: !this.state.applyAnnotationToSeries, + }) + } + /> + diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap index 3b93213da4033..63ec1744b62d0 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap @@ -11,7 +11,7 @@ exports[`AnnotationsTable Initialization with annotations prop. 1`] = ` "name": "Annotation", "scope": "row", "sortable": true, - "width": "50%", + "width": "40%", }, Object { "dataType": "date", @@ -39,6 +39,27 @@ exports[`AnnotationsTable Initialization with annotations prop. 1`] = ` "name": "Last modified by", "sortable": true, }, + Object { + "field": "event", + "name": "Event", + "sortable": true, + "width": "10%", + }, + Object { + "field": "partition_field_value", + "name": "Partition", + "sortable": true, + }, + Object { + "field": "over_field_value", + "name": "Over", + "sortable": true, + }, + Object { + "field": "by_field_value", + "name": "By", + "sortable": true, + }, Object { "actions": Array [ Object { @@ -52,6 +73,12 @@ exports[`AnnotationsTable Initialization with annotations prop. 1`] = ` "name": "Actions", "width": "60px", }, + Object { + "dataType": "boolean", + "field": "current_series", + "name": "current_series", + "width": "0px", + }, ] } compressed={true} @@ -82,6 +109,24 @@ exports[`AnnotationsTable Initialization with annotations prop. 1`] = ` } responsive={true} rowProps={[Function]} + search={ + Object { + "box": Object { + "incremental": true, + "schema": true, + }, + "defaultQuery": "event:(user or delayed_data)", + "filters": Array [ + Object { + "field": "event", + "multiSelect": "or", + "name": "Event", + "options": Array [], + "type": "field_value_selection", + }, + ], + } + } sorting={ Object { "sort": Object { diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index a091da6c359d1..cf4d25f159a1a 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -9,11 +9,9 @@ * This version supports both fetching the annotations by itself (used in the jobs list) and * getting the annotations via props (used in Anomaly Explorer and Single Series Viewer). */ - import _ from 'lodash'; import PropTypes from 'prop-types'; import rison from 'rison-node'; - import React, { Component, Fragment } from 'react'; import { @@ -50,7 +48,12 @@ import { annotationsRefresh$, annotationsRefreshed, } from '../../../services/annotations_service'; +import { + ANNOTATION_EVENT_USER, + ANNOTATION_EVENT_DELAYED_DATA, +} from '../../../../../common/constants/annotations'; +const CURRENT_SERIES = 'current_series'; /** * Table component for rendering the lists of annotations for an ML job. */ @@ -66,7 +69,10 @@ export class AnnotationsTable extends Component { super(props); this.state = { annotations: [], + aggregations: null, isLoading: false, + queryText: `event:(${ANNOTATION_EVENT_USER} or ${ANNOTATION_EVENT_DELAYED_DATA})`, + searchError: undefined, jobId: Array.isArray(this.props.jobs) && this.props.jobs.length > 0 && @@ -74,6 +80,9 @@ export class AnnotationsTable extends Component { ? this.props.jobs[0].job_id : undefined, }; + this.sorting = { + sort: { field: 'timestamp', direction: 'asc' }, + }; } getAnnotations() { @@ -92,11 +101,18 @@ export class AnnotationsTable extends Component { earliestMs: null, latestMs: null, maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, + fields: [ + { + field: 'event', + missing: ANNOTATION_EVENT_USER, + }, + ], }) .toPromise() .then((resp) => { this.setState((prevState, props) => ({ annotations: resp.annotations[props.jobs[0].job_id] || [], + aggregations: resp.aggregations, errorMessage: undefined, isLoading: false, jobId: props.jobs[0].job_id, @@ -114,6 +130,25 @@ export class AnnotationsTable extends Component { } } + getAnnotationsWithExtraInfo(annotations) { + // if there is a specific view/chart entities that the annotations can be scoped to + // add a new column called 'current_series' + if (Array.isArray(this.props.chartDetails?.entityData?.entities)) { + return annotations.map((annotation) => { + const allMatched = this.props.chartDetails?.entityData?.entities.every( + ({ fieldType, fieldValue }) => { + const field = `${fieldType}_value`; + return !(!annotation[field] || annotation[field] !== fieldValue); + } + ); + return { ...annotation, [CURRENT_SERIES]: allMatched }; + }); + } else { + // if not make it return the original annotations + return annotations; + } + } + getJob(jobId) { // check if the job was supplied via props and matches the supplied jobId if (Array.isArray(this.props.jobs) && this.props.jobs.length > 0) { @@ -134,9 +169,9 @@ export class AnnotationsTable extends Component { Array.isArray(this.props.jobs) && this.props.jobs.length > 0 ) { - this.annotationsRefreshSubscription = annotationsRefresh$.subscribe(() => - this.getAnnotations() - ); + this.annotationsRefreshSubscription = annotationsRefresh$.subscribe(() => { + this.getAnnotations(); + }); annotationsRefreshed(); } } @@ -198,9 +233,11 @@ export class AnnotationsTable extends Component { }, }, }; + let mlTimeSeriesExplorer = {}; + const entityCondition = {}; if (annotation.timestamp !== undefined && annotation.end_timestamp !== undefined) { - appState.mlTimeSeriesExplorer = { + mlTimeSeriesExplorer = { zoom: { from: new Date(annotation.timestamp).toISOString(), to: new Date(annotation.end_timestamp).toISOString(), @@ -216,6 +253,27 @@ export class AnnotationsTable extends Component { } } + // if the annotation is at the series level + // then pass the partitioning field(s) and detector index to the Single Metric Viewer + if (_.has(annotation, 'detector_index')) { + mlTimeSeriesExplorer.detector_index = annotation.detector_index; + } + if (_.has(annotation, 'partition_field_value')) { + entityCondition[annotation.partition_field_name] = annotation.partition_field_value; + } + + if (_.has(annotation, 'over_field_value')) { + entityCondition[annotation.over_field_name] = annotation.over_field_value; + } + + if (_.has(annotation, 'by_field_value')) { + // Note that analyses with by and over fields, will have a top-level by_field_name, + // but the by_field_value(s) will be in the nested causes array. + entityCondition[annotation.by_field_name] = annotation.by_field_value; + } + mlTimeSeriesExplorer.entities = entityCondition; + appState.mlTimeSeriesExplorer = mlTimeSeriesExplorer; + const _g = rison.encode(globalSettings); const _a = rison.encode(appState); @@ -251,6 +309,8 @@ export class AnnotationsTable extends Component { render() { const { isSingleMetricViewerLinkVisible = true, isNumberBadgeVisible = false } = this.props; + const { queryText, searchError } = this.state; + if (this.props.annotations === undefined) { if (this.state.isLoading === true) { return ( @@ -314,7 +374,7 @@ export class AnnotationsTable extends Component { defaultMessage: 'Annotation', }), sortable: true, - width: '50%', + width: '40%', scope: 'row', }, { @@ -351,6 +411,14 @@ export class AnnotationsTable extends Component { }), sortable: true, }, + { + field: 'event', + name: i18n.translate('xpack.ml.annotationsTable.eventColumnName', { + defaultMessage: 'Event', + }), + sortable: true, + width: '10%', + }, ]; const jobIds = _.uniq(annotations.map((a) => a.job_id)); @@ -382,22 +450,23 @@ export class AnnotationsTable extends Component { actions.push({ render: (annotation) => { + // find the original annotation because the table might not show everything + const annotationId = annotation._id; + const originalAnnotation = annotations.find((d) => d._id === annotationId); const editAnnotationsTooltipText = ( ); - const editAnnotationsTooltipAriaLabelText = ( - + const editAnnotationsTooltipAriaLabelText = i18n.translate( + 'xpack.ml.annotationsTable.editAnnotationsTooltipAriaLabel', + { defaultMessage: 'Edit annotation' } ); return ( annotation$.next(annotation)} + onClick={() => annotation$.next(originalAnnotation ?? annotation)} iconType="pencil" aria-label={editAnnotationsTooltipAriaLabelText} /> @@ -421,17 +490,14 @@ export class AnnotationsTable extends Component { defaultMessage="Job configuration not supported in Single Metric Viewer" /> ); - const openInSingleMetricViewerAriaLabelText = isDrillDownAvailable ? ( - - ) : ( - - ); + const openInSingleMetricViewerAriaLabelText = isDrillDownAvailable + ? i18n.translate('xpack.ml.annotationsTable.openInSingleMetricViewerAriaLabel', { + defaultMessage: 'Open in Single Metric Viewer', + }) + : i18n.translate( + 'xpack.ml.annotationsTable.jobConfigurationNotSupportedInSingleMetricViewerAriaLabel', + { defaultMessage: 'Job configuration not supported in Single Metric Viewer' } + ); return ( @@ -447,38 +513,152 @@ export class AnnotationsTable extends Component { }); } - columns.push({ - align: RIGHT_ALIGNMENT, - width: '60px', - name: i18n.translate('xpack.ml.annotationsTable.actionsColumnName', { - defaultMessage: 'Actions', - }), - actions, - }); - const getRowProps = (item) => { return { onMouseOver: () => this.onMouseOverRow(item), onMouseLeave: () => this.onMouseLeaveRow(), }; }; + let filterOptions = []; + const aggregations = this.props.aggregations ?? this.state.aggregations; + if (aggregations) { + const buckets = aggregations.event.buckets; + const foundUser = buckets.findIndex((d) => d.key === ANNOTATION_EVENT_USER) > -1; + filterOptions = foundUser + ? buckets + : [{ key: ANNOTATION_EVENT_USER, doc_count: 0 }, ...buckets]; + } + const filters = [ + { + type: 'field_value_selection', + field: 'event', + name: 'Event', + multiSelect: 'or', + options: filterOptions.map((field) => ({ + value: field.key, + name: field.key, + view: `${field.key} (${field.doc_count})`, + })), + }, + ]; + + if (this.props.detectors) { + columns.push({ + name: i18n.translate('xpack.ml.annotationsTable.detectorColumnName', { + defaultMessage: 'Detector', + }), + width: '10%', + render: (item) => { + if ('detector_index' in item) { + return this.props.detectors[item.detector_index].detector_description; + } + return ''; + }, + }); + } + if (Array.isArray(this.props.chartDetails?.entityData?.entities)) { + // only show the column if the field exists in that job in SMV + this.props.chartDetails?.entityData?.entities.forEach((entity) => { + if (entity.fieldType === 'partition_field') { + columns.push({ + field: 'partition_field_value', + name: i18n.translate('xpack.ml.annotationsTable.partitionSMVColumnName', { + defaultMessage: 'Partition', + }), + sortable: true, + }); + } + if (entity.fieldType === 'over_field') { + columns.push({ + field: 'over_field_value', + name: i18n.translate('xpack.ml.annotationsTable.overColumnSMVName', { + defaultMessage: 'Over', + }), + sortable: true, + }); + } + if (entity.fieldType === 'by_field') { + columns.push({ + field: 'by_field_value', + name: i18n.translate('xpack.ml.annotationsTable.byColumnSMVName', { + defaultMessage: 'By', + }), + sortable: true, + }); + } + }); + filters.push({ + type: 'is', + field: CURRENT_SERIES, + name: i18n.translate('xpack.ml.annotationsTable.seriesOnlyFilterName', { + defaultMessage: 'Filter to series', + }), + }); + } else { + // else show all the partition columns in AE because there might be multiple jobs + columns.push({ + field: 'partition_field_value', + name: i18n.translate('xpack.ml.annotationsTable.partitionAEColumnName', { + defaultMessage: 'Partition', + }), + sortable: true, + }); + columns.push({ + field: 'over_field_value', + name: i18n.translate('xpack.ml.annotationsTable.overAEColumnName', { + defaultMessage: 'Over', + }), + sortable: true, + }); + + columns.push({ + field: 'by_field_value', + name: i18n.translate('xpack.ml.annotationsTable.byAEColumnName', { + defaultMessage: 'By', + }), + sortable: true, + }); + } + const search = { + defaultQuery: queryText, + box: { + incremental: true, + schema: true, + }, + filters: filters, + }; + + columns.push( + { + align: RIGHT_ALIGNMENT, + width: '60px', + name: i18n.translate('xpack.ml.annotationsTable.actionsColumnName', { + defaultMessage: 'Actions', + }), + actions, + }, + { + // hidden column, for search only + field: CURRENT_SERIES, + name: CURRENT_SERIES, + dataType: 'boolean', + width: '0px', + } + ); return ( diff --git a/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts b/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts index 095b42ffac5b7..3fcb032bd3ce1 100644 --- a/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts +++ b/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts @@ -258,7 +258,7 @@ const loadExplorerDataProvider = (anomalyTimelineService: AnomalyTimelineService { influencers, viewBySwimlaneState } ): Partial => { return { - annotationsData, + annotations: annotationsData, influencers, loading: false, viewBySwimlaneDataLoading: false, diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.d.ts b/x-pack/plugins/ml/public/application/explorer/explorer.d.ts index 90fb46d3cec4a..52181aab40328 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.d.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer.d.ts @@ -5,11 +5,6 @@ */ import { FC } from 'react'; - -import { UrlState } from '../util/url_state'; - -import { JobSelection } from '../components/job_selector/use_job_selection'; - import { ExplorerState } from './reducers'; import { AppStateSelectedCells } from './explorer_utils'; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index df4cea0c07987..4e27c17631506 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -14,6 +14,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { + htmlIdGenerator, EuiFlexGroup, EuiFlexItem, EuiFormRow, @@ -26,6 +27,9 @@ import { EuiSpacer, EuiTitle, EuiLoadingContent, + EuiPanel, + EuiAccordion, + EuiBadge, } from '@elastic/eui'; import { AnnotationFlyout } from '../components/annotations/annotation_flyout'; @@ -138,6 +142,7 @@ export class Explorer extends React.Component { }; state = { filterIconTriggeredQuery: undefined, language: DEFAULT_QUERY_LANG }; + htmlIdGen = htmlIdGenerator(); // Escape regular parens from fieldName as that portion of the query is not wrapped in double quotes // and will cause a syntax error when called with getKqlQueryValues @@ -202,7 +207,7 @@ export class Explorer extends React.Component { const { showCharts, severity } = this.props; const { - annotationsData, + annotations, chartsData, filterActive, filterPlaceHolder, @@ -216,6 +221,7 @@ export class Explorer extends React.Component { selectedJobs, tableData, } = this.props.explorerState; + const { annotationsData, aggregations } = annotations; const jobSelectorProps = { dateFormatTz: getDateFormatTz(), @@ -239,13 +245,12 @@ export class Explorer extends React.Component { ); } - const mainColumnWidthClassName = noInfluencersConfigured === true ? 'col-xs-12' : 'col-xs-10'; const mainColumnClasses = `column ${mainColumnWidthClassName}`; const timefilter = getTimefilter(); const bounds = timefilter.getActiveBounds(); - + const selectedJobIds = Array.isArray(selectedJobs) ? selectedJobs.map((job) => job.id) : []; return ( - {annotationsData.length > 0 && ( <> - -

- -

-
- + + +

+ + + + ), + }} + /> +

+ + } + > + <> + + +
+
- + )} - {loading === false && ( - <> +

+ )} + +
{showCharts && }
+ - +
)} diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js index 23da9669ee9a5..6e0863f1a6e5b 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js @@ -34,6 +34,7 @@ import { SWIMLANE_TYPE, VIEW_BY_JOB_LABEL, } from './explorer_constants'; +import { ANNOTATION_EVENT_USER } from '../../../common/constants/annotations'; // create new job objects based on standard job config objects // new job objects just contain job id, bucket span in seconds and a selected flag. @@ -395,6 +396,12 @@ export function loadAnnotationsTableData(selectedCells, selectedJobs, interval, earliestMs: timeRange.earliestMs, latestMs: timeRange.latestMs, maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, + fields: [ + { + field: 'event', + missing: ANNOTATION_EVENT_USER, + }, + ], }) .toPromise() .then((resp) => { @@ -410,16 +417,17 @@ export function loadAnnotationsTableData(selectedCells, selectedJobs, interval, } }); - return resolve( - annotationsData + return resolve({ + annotationsData: annotationsData .sort((a, b) => { return a.timestamp - b.timestamp; }) .map((d, i) => { d.key = String.fromCharCode(65 + i); return d; - }) - ); + }), + aggregations: resp.aggregations, + }); }) .catch((resp) => { console.log('Error loading list of annotations for jobs list:', resp); diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts index c55c06c80ab81..a38044a8b3425 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts @@ -113,7 +113,7 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo const { annotationsData, overallState, tableData } = payload; nextState = { ...state, - annotationsData, + annotations: annotationsData, overallSwimlaneData: overallState, tableData, viewBySwimlaneData: { diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts index 892b46467345b..889d572f4fabc 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts @@ -21,10 +21,14 @@ import { SwimlaneData, ViewBySwimLaneData, } from '../../explorer_utils'; +import { Annotations, EsAggregationResult } from '../../../../../common/types/annotations'; import { SWIM_LANE_DEFAULT_PAGE_SIZE } from '../../explorer_constants'; export interface ExplorerState { - annotationsData: any[]; + annotations: { + annotationsData: Annotations; + aggregations: EsAggregationResult; + }; bounds: TimeRangeBounds | undefined; chartsData: ExplorerChartsData; fieldFormatsLoading: boolean; @@ -62,7 +66,10 @@ function getDefaultIndexPattern() { export function getExplorerDefaultState(): ExplorerState { return { - annotationsData: [], + annotations: { + annotationsData: [], + aggregations: {}, + }, bounds: undefined, chartsData: getDefaultChartsData(), fieldFormatsLoading: false, diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index 5c22a440a103e..7d09797a0ff1b 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -157,7 +157,6 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim }, [explorerAppState]); const explorerState = useObservable(explorerService.state$); - const [showCharts] = useShowCharts(); const [tableInterval] = useTableInterval(); const [tableSeverity] = useTableSeverity(); diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/annotations.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/annotations.ts index 29a5732026761..f9e19ba6f757e 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/annotations.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/annotations.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Annotation } from '../../../../common/types/annotations'; +import { + Annotation, + FieldToBucket, + GetAnnotationsResponse, +} from '../../../../common/types/annotations'; import { http, http$ } from '../http_service'; import { basePath } from './index'; @@ -14,15 +18,19 @@ export const annotations = { earliestMs: number; latestMs: number; maxAnnotations: number; + fields: FieldToBucket[]; + detectorIndex: number; + entities: any[]; }) { const body = JSON.stringify(obj); - return http$<{ annotations: Record }>({ + return http$({ path: `${basePath()}/annotations`, method: 'POST', body, }); }, - indexAnnotation(obj: any) { + + indexAnnotation(obj: Annotation) { const body = JSON.stringify(obj); return http({ path: `${basePath()}/annotations/index`, diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index d4470e7502e0d..95dc1ed6988f6 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -28,6 +28,8 @@ import { EuiSelect, EuiSpacer, EuiTitle, + EuiAccordion, + EuiBadge, } from '@elastic/eui'; import { getToastNotifications } from '../util/dependency_cache'; @@ -125,6 +127,8 @@ function getTimeseriesexplorerDefaultState() { entitiesLoading: false, entityValues: {}, focusAnnotationData: [], + focusAggregations: {}, + focusAggregationInterval: {}, focusChartData: undefined, focusForecastData: undefined, fullRefresh: true, @@ -1025,6 +1029,7 @@ export class TimeSeriesExplorer extends React.Component { entityValues, focusAggregationInterval, focusAnnotationData, + focusAggregations, focusChartData, focusForecastData, fullRefresh, @@ -1075,8 +1080,8 @@ export class TimeSeriesExplorer extends React.Component { const entityControls = this.getControlsForDetector(); const fieldNamesWithEmptyValues = this.getFieldNamesWithEmptyValues(); const arePartitioningFieldsProvided = this.arePartitioningFieldsProvided(); - - const detectorSelectOptions = getViewableDetectors(selectedJob).map((d) => ({ + const detectors = getViewableDetectors(selectedJob); + const detectorSelectOptions = detectors.map((d) => ({ value: d.index, text: d.detector_description, })); @@ -1311,25 +1316,49 @@ export class TimeSeriesExplorer extends React.Component { )} - {showAnnotations && focusAnnotationData.length > 0 && ( -
- -

- -

-
+ {focusAnnotationData && focusAnnotationData.length > 0 && ( + +

+ + + + ), + }} + /> +

+ + } + > -
+ )} - +

number; @@ -37,6 +38,7 @@ export interface FocusData { showForecastCheckbox?: any; focusAnnotationData?: any; focusForecastData?: any; + focusAggregations?: any; } export function getFocusData( @@ -84,11 +86,23 @@ export function getFocusData( earliestMs: searchBounds.min.valueOf(), latestMs: searchBounds.max.valueOf(), maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, + fields: [ + { + field: 'event', + missing: ANNOTATION_EVENT_USER, + }, + ], + detectorIndex, + entities: nonBlankEntities, }) .pipe( catchError(() => { // silent fail - return of({ annotations: {} as Record }); + return of({ + annotations: {} as Record, + aggregations: {}, + success: false, + }); }) ), // Plus query for forecast data if there is a forecastId stored in the appState. @@ -146,13 +160,14 @@ export function getFocusData( d.key = String.fromCharCode(65 + i); return d; }); + + refreshFocusData.focusAggregations = annotations.aggregations; } if (forecastData) { refreshFocusData.focusForecastData = processForecastResults(forecastData.results); refreshFocusData.showForecastCheckbox = refreshFocusData.focusForecastData.length > 0; } - return refreshFocusData; }) ); diff --git a/x-pack/plugins/ml/server/models/annotation_service/annotation.ts b/x-pack/plugins/ml/server/models/annotation_service/annotation.ts index c2582107062bb..f7353034b7453 100644 --- a/x-pack/plugins/ml/server/models/annotation_service/annotation.ts +++ b/x-pack/plugins/ml/server/models/annotation_service/annotation.ts @@ -8,7 +8,8 @@ import Boom from 'boom'; import _ from 'lodash'; import { ILegacyScopedClusterClient } from 'kibana/server'; -import { ANNOTATION_TYPE } from '../../../common/constants/annotations'; +import { ANNOTATION_EVENT_USER, ANNOTATION_TYPE } from '../../../common/constants/annotations'; +import { PARTITION_FIELDS } from '../../../common/constants/anomalies'; import { ML_ANNOTATIONS_INDEX_ALIAS_READ, ML_ANNOTATIONS_INDEX_ALIAS_WRITE, @@ -19,20 +20,35 @@ import { Annotations, isAnnotation, isAnnotations, + getAnnotationFieldName, + getAnnotationFieldValue, + EsAggregationResult, } from '../../../common/types/annotations'; // TODO All of the following interface/type definitions should // eventually be replaced by the proper upstream definitions interface EsResult { - _source: object; + _source: Annotation; _id: string; } +export interface FieldToBucket { + field: string; + missing?: string | number; +} + export interface IndexAnnotationArgs { jobIds: string[]; earliestMs: number; latestMs: number; maxAnnotations: number; + fields?: FieldToBucket[]; + detectorIndex?: number; + entities?: any[]; +} + +export interface AggTerm { + terms: FieldToBucket; } export interface GetParams { @@ -43,9 +59,8 @@ export interface GetParams { export interface GetResponse { success: true; - annotations: { - [key: string]: Annotations; - }; + annotations: Record; + aggregations: EsAggregationResult; } export interface IndexParams { @@ -96,10 +111,14 @@ export function annotationProvider({ callAsCurrentUser }: ILegacyScopedClusterCl earliestMs, latestMs, maxAnnotations, + fields, + detectorIndex, + entities, }: IndexAnnotationArgs) { const obj: GetResponse = { success: true, annotations: {}, + aggregations: {}, }; const boolCriteria: object[] = []; @@ -182,6 +201,64 @@ export function annotationProvider({ callAsCurrentUser }: ILegacyScopedClusterCl }); } + // Find unique buckets (e.g. events) from the queried annotations to show in dropdowns + const aggs: Record = {}; + if (fields) { + fields.forEach((fieldToBucket) => { + aggs[fieldToBucket.field] = { + terms: { + ...fieldToBucket, + }, + }; + }); + } + + // Build should clause to further query for annotations in SMV + // we want to show either the exact match with detector index and by/over/partition fields + // OR annotations without any partition fields defined + let shouldClauses; + if (detectorIndex !== undefined && Array.isArray(entities)) { + // build clause to get exact match of detector index and by/over/partition fields + const beExactMatch = []; + beExactMatch.push({ + term: { + detector_index: detectorIndex, + }, + }); + + entities.forEach(({ fieldName, fieldType, fieldValue }) => { + beExactMatch.push({ + term: { + [getAnnotationFieldName(fieldType)]: fieldName, + }, + }); + beExactMatch.push({ + term: { + [getAnnotationFieldValue(fieldType)]: fieldValue, + }, + }); + }); + + // clause to get annotations that have no partition fields + const haveAnyPartitionFields: object[] = []; + PARTITION_FIELDS.forEach((field) => { + haveAnyPartitionFields.push({ + exists: { + field: getAnnotationFieldName(field), + }, + }); + haveAnyPartitionFields.push({ + exists: { + field: getAnnotationFieldValue(field), + }, + }); + }); + shouldClauses = [ + { bool: { must_not: haveAnyPartitionFields } }, + { bool: { must: beExactMatch } }, + ]; + } + const params: GetParams = { index: ML_ANNOTATIONS_INDEX_ALIAS_READ, size: maxAnnotations, @@ -201,8 +278,10 @@ export function annotationProvider({ callAsCurrentUser }: ILegacyScopedClusterCl }, }, ], + ...(shouldClauses ? { should: shouldClauses, minimum_should_match: 1 } : {}), }, }, + ...(fields ? { aggs } : {}), }, }; @@ -217,9 +296,19 @@ export function annotationProvider({ callAsCurrentUser }: ILegacyScopedClusterCl const docs: Annotations = _.get(resp, ['hits', 'hits'], []).map((d: EsResult) => { // get the original source document and the document id, we need it // to identify the annotation when editing/deleting it. - return { ...d._source, _id: d._id } as Annotation; + // if original `event` is undefined then substitute with 'user` by default + // since annotation was probably generated by user on the UI + return { + ...d._source, + event: d._source?.event ?? ANNOTATION_EVENT_USER, + _id: d._id, + } as Annotation; }); + const aggregations = _.get(resp, ['aggregations'], {}) as EsAggregationResult; + if (fields) { + obj.aggregations = aggregations; + } if (isAnnotations(docs) === false) { // No need to translate, this will not be exposed in the UI. throw new Error(`Annotations didn't pass integrity check.`); diff --git a/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts b/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts index d7403c45f1be2..663ee846571e7 100644 --- a/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts +++ b/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts @@ -6,13 +6,11 @@ import Boom from 'boom'; import { ILegacyScopedClusterClient } from 'kibana/server'; +import { PARTITION_FIELDS } from '../../../common/constants/anomalies'; +import { PartitionFieldsType } from '../../../common/types/anomalies'; import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; import { CriteriaField } from './results_service'; -const PARTITION_FIELDS = ['partition_field', 'over_field', 'by_field'] as const; - -type PartitionFieldsType = typeof PARTITION_FIELDS[number]; - type SearchTerm = | { [key in PartitionFieldsType]?: string; diff --git a/x-pack/plugins/ml/server/routes/schemas/annotations_schema.ts b/x-pack/plugins/ml/server/routes/schemas/annotations_schema.ts index fade2093ac842..14a2f632419bc 100644 --- a/x-pack/plugins/ml/server/routes/schemas/annotations_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/annotations_schema.ts @@ -16,6 +16,14 @@ export const indexAnnotationSchema = schema.object({ create_username: schema.maybe(schema.string()), modified_time: schema.maybe(schema.number()), modified_username: schema.maybe(schema.string()), + event: schema.maybe(schema.string()), + detector_index: schema.maybe(schema.number()), + partition_field_name: schema.maybe(schema.string()), + partition_field_value: schema.maybe(schema.string()), + over_field_name: schema.maybe(schema.string()), + over_field_value: schema.maybe(schema.string()), + by_field_name: schema.maybe(schema.string()), + by_field_value: schema.maybe(schema.string()), /** Document id */ _id: schema.maybe(schema.string()), key: schema.maybe(schema.string()), @@ -26,6 +34,25 @@ export const getAnnotationsSchema = schema.object({ earliestMs: schema.oneOf([schema.nullable(schema.number()), schema.maybe(schema.number())]), latestMs: schema.oneOf([schema.nullable(schema.number()), schema.maybe(schema.number())]), maxAnnotations: schema.number(), + /** Fields to find unique values for (e.g. events or created_by) */ + fields: schema.maybe( + schema.arrayOf( + schema.object({ + field: schema.string(), + missing: schema.maybe(schema.string()), + }) + ) + ), + detectorIndex: schema.maybe(schema.number()), + entities: schema.maybe( + schema.arrayOf( + schema.object({ + fieldType: schema.maybe(schema.string()), + fieldName: schema.maybe(schema.string()), + fieldValue: schema.maybe(schema.string()), + }) + ) + ), }); export const deleteAnnotationSchema = schema.object({ annotationId: schema.string() }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c8fe792af926d..287cf443b1b07 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9689,7 +9689,6 @@ "xpack.ml.datavisualizerBreadcrumbLabel": "データビジュアライザー", "xpack.ml.dataVisualizerPageLabel": "データビジュアライザー", "xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.errorMessage": "メッセージを読み込めませんでした", - "xpack.ml.explorer.annotationsTitle": "注釈", "xpack.ml.explorer.anomaliesTitle": "異常", "xpack.ml.explorer.anomalyTimelineTitle": "異常のタイムライン", "xpack.ml.explorer.charts.detectorLabel": "「{fieldName}」で分割された {detectorLabel}{br} Y 軸イベントの分布", @@ -10802,7 +10801,6 @@ "xpack.ml.timeSeriesExplorer.annotationFlyout.noAnnotationTextError": "注釈テキストを入力してください", "xpack.ml.timeSeriesExplorer.annotationFlyout.updateButtonLabel": "更新", "xpack.ml.timeSeriesExplorer.annotationsLabel": "注釈", - "xpack.ml.timeSeriesExplorer.annotationsTitle": "注釈", "xpack.ml.timeSeriesExplorer.anomaliesTitle": "異常", "xpack.ml.timeSeriesExplorer.autoSelectingFirstJobText": "、初めのジョブを自動選択します", "xpack.ml.timeSeriesExplorer.canNotViewRequestedJobsWarningMessage": "リクエストされた‘{invalidIdsCount, plural, one {ジョブ} other {件のジョブ}} {invalidIds} をこのダッシュボードで表示できません", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 7640675a427ce..ea3aa71b154aa 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9694,7 +9694,6 @@ "xpack.ml.datavisualizerBreadcrumbLabel": "数据可视化工具", "xpack.ml.dataVisualizerPageLabel": "数据可视化工具", "xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.errorMessage": "无法加载消息", - "xpack.ml.explorer.annotationsTitle": "注释", "xpack.ml.explorer.anomaliesTitle": "异常", "xpack.ml.explorer.anomalyTimelineTitle": "异常时间线", "xpack.ml.explorer.charts.detectorLabel": "{detectorLabel}{br}y 轴事件分布按 “{fieldName}” 分割", @@ -10807,7 +10806,6 @@ "xpack.ml.timeSeriesExplorer.annotationFlyout.noAnnotationTextError": "输入注释文本", "xpack.ml.timeSeriesExplorer.annotationFlyout.updateButtonLabel": "更新", "xpack.ml.timeSeriesExplorer.annotationsLabel": "注释", - "xpack.ml.timeSeriesExplorer.annotationsTitle": "注释", "xpack.ml.timeSeriesExplorer.anomaliesTitle": "异常", "xpack.ml.timeSeriesExplorer.autoSelectingFirstJobText": ",自动选择第一个作业", "xpack.ml.timeSeriesExplorer.canNotViewRequestedJobsWarningMessage": "您无法在此仪表板中查看请求的 {invalidIdsCount, plural, one {作业} other {作业}} {invalidIds}", From 8f8736cce87945d6cac68fb714c1f21fc81ebcf2 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Tue, 14 Jul 2020 12:45:15 -0500 Subject: [PATCH 20/82] Fix bug where lists "needs configuration" while index is being created (#71653) The behavior here was that you'd be redirected to detections from wherever you were, with no warning/indication. When we knew we needed an index, and that we could create one, needsConfiguration was incorrectly 'true' during the time between realizing this fact and creating the index. That intermediate state is now captured in needsIndexConfiguration, which is true if we either can't create the index or we failed our attempt to do so. --- .../detection_engine/lists/use_lists_config.tsx | 9 ++++++--- .../detection_engine/lists/use_lists_index.tsx | 10 +++++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.tsx index ea5e075811d4b..e21cbceeaef27 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.tsx @@ -19,17 +19,20 @@ export interface UseListsConfigReturn { } export const useListsConfig = (): UseListsConfigReturn => { - const { createIndex, indexExists, loading: indexLoading } = useListsIndex(); + const { createIndex, createIndexError, indexExists, loading: indexLoading } = useListsIndex(); const { canManageIndex, canWriteIndex, loading: privilegesLoading } = useListsPrivileges(); const { lists } = useKibana().services; const enabled = lists != null; const loading = indexLoading || privilegesLoading; const needsIndex = indexExists === false; - const needsConfiguration = !enabled || needsIndex || canWriteIndex === false; + const indexCreationFailed = createIndexError != null; + const needsIndexConfiguration = + needsIndex && (canManageIndex === false || (canManageIndex === true && indexCreationFailed)); + const needsConfiguration = !enabled || canWriteIndex === false || needsIndexConfiguration; useEffect(() => { - if (canManageIndex && needsIndex) { + if (needsIndex && canManageIndex) { createIndex(); } }, [canManageIndex, createIndex, needsIndex]); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx index a9497fd4971c1..75f12bd07d3ae 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx @@ -18,6 +18,8 @@ export interface UseListsIndexState { export interface UseListsIndexReturn extends UseListsIndexState { loading: boolean; createIndex: () => void; + createIndexError: unknown; + createIndexResult: { acknowledged: boolean } | undefined; } export const useListsIndex = (): UseListsIndexReturn => { @@ -96,5 +98,11 @@ export const useListsIndex = (): UseListsIndexReturn => { } }, [createListIndexState.error, toasts]); - return { loading, createIndex, ...state }; + return { + loading, + createIndex, + createIndexError: createListIndexState.error, + createIndexResult: createListIndexState.result, + ...state, + }; }; From 981d678e4207a4d850ae2b4b7fba3cb69a499e59 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 14 Jul 2020 19:53:14 +0200 Subject: [PATCH 21/82] [Uptime] Duration Anomaly Alert (#71208) --- .../providers/results_service.ts | 9 +- .../plugins/uptime/common/constants/alerts.ts | 5 + .../uptime/common/constants/rest_api.ts | 2 + .../lib/__tests__/ml.test.ts} | 2 +- x-pack/plugins/uptime/common/lib/index.ts | 2 + x-pack/plugins/uptime/common/lib/ml.ts | 27 ++++ x-pack/plugins/uptime/kibana.json | 2 +- .../ml/__tests__/ml_manage_job.test.tsx | 8 +- .../monitor/ml/confirm_alert_delete.tsx | 38 +++++ .../components/monitor/ml/manage_ml_job.tsx | 62 ++++++-- .../monitor/ml/ml_flyout_container.tsx | 74 +++++----- .../components/monitor/ml/ml_integeration.tsx | 2 +- .../components/monitor/ml/ml_job_link.tsx | 2 +- .../components/monitor/ml/translations.tsx | 14 ++ .../monitor/ml/use_anomaly_alert.ts | 30 ++++ .../monitor_duration_container.tsx | 2 +- .../alerts/alert_expression_popover.tsx | 2 +- .../alerts/anomaly_alert/anomaly_alert.tsx | 86 +++++++++++ .../alerts/anomaly_alert/select_severity.tsx | 135 ++++++++++++++++++ .../alerts/anomaly_alert/translations.ts | 26 ++++ .../lib/alert_types/duration_anomaly.tsx | 37 +++++ .../uptime/public/lib/alert_types/index.ts | 2 + .../public/lib/alert_types/translations.ts | 22 ++- .../plugins/uptime/public/pages/monitor.tsx | 5 + .../uptime/public/state/actions/alerts.ts | 15 ++ .../plugins/uptime/public/state/actions/ui.ts | 2 + .../plugins/uptime/public/state/api/alerts.ts | 27 ++++ .../uptime/public/state/api/ml_anomaly.ts | 27 +--- .../uptime/public/state/effects/alerts.ts | 39 +++++ .../uptime/public/state/effects/index.ts | 2 + .../uptime/public/state/effects/ml_anomaly.ts | 26 +++- .../uptime/public/state/kibana_service.ts | 4 + .../__tests__/__snapshots__/ui.test.ts.snap | 2 + .../state/reducers/__tests__/ui.test.ts | 6 + .../uptime/public/state/reducers/alerts.ts | 29 ++++ .../uptime/public/state/reducers/index.ts | 2 + .../uptime/public/state/reducers/ui.ts | 7 + .../state/selectors/__tests__/index.test.ts | 5 + .../uptime/public/state/selectors/index.ts | 6 + .../lib/adapters/framework/adapter_types.ts | 2 + .../lib/alerts/__tests__/status_check.test.ts | 41 +++--- .../server/lib/alerts/duration_anomaly.ts | 129 +++++++++++++++++ .../plugins/uptime/server/lib/alerts/index.ts | 2 + .../uptime/server/lib/alerts/translations.ts | 90 ++++++++++++ .../plugins/uptime/server/lib/alerts/types.ts | 8 +- x-pack/plugins/uptime/server/uptime_server.ts | 2 +- .../functional/services/uptime/ml_anomaly.ts | 20 +++ .../apps/uptime/anomaly_alert.ts | 131 +++++++++++++++++ .../apps/uptime/index.ts | 1 + 49 files changed, 1109 insertions(+), 112 deletions(-) rename x-pack/plugins/uptime/{public/state/api/__tests__/ml_anomaly.test.ts => common/lib/__tests__/ml.test.ts} (95%) create mode 100644 x-pack/plugins/uptime/common/lib/ml.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor/ml/confirm_alert_delete.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/ml/use_anomaly_alert.ts create mode 100644 x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/anomaly_alert.tsx create mode 100644 x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/select_severity.tsx create mode 100644 x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/translations.ts create mode 100644 x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx create mode 100644 x-pack/plugins/uptime/public/state/actions/alerts.ts create mode 100644 x-pack/plugins/uptime/public/state/api/alerts.ts create mode 100644 x-pack/plugins/uptime/public/state/effects/alerts.ts create mode 100644 x-pack/plugins/uptime/public/state/reducers/alerts.ts create mode 100644 x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts create mode 100644 x-pack/test/functional_with_es_ssl/apps/uptime/anomaly_alert.ts diff --git a/x-pack/plugins/ml/server/shared_services/providers/results_service.ts b/x-pack/plugins/ml/server/shared_services/providers/results_service.ts index 366a1f8b8c6f4..6af4eb008567a 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/results_service.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/results_service.ts @@ -25,7 +25,14 @@ export function getResultsServiceProvider({ }: SharedServicesChecks): ResultsServiceProvider { return { resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClient, request: KibanaRequest) { - const hasMlCapabilities = getHasMlCapabilities(request); + // Uptime is using this service in anomaly alert, kibana alerting doesn't provide request object + // So we are adding a dummy request for now + // TODO: Remove this once kibana alerting provides request object + const hasMlCapabilities = + request.params !== 'DummyKibanaRequest' + ? getHasMlCapabilities(request) + : (_caps: string[]) => Promise.resolve(); + const { getAnomaliesTableData } = resultsServiceProvider(mlClusterClient); return { async getAnomaliesTableData(...args) { diff --git a/x-pack/plugins/uptime/common/constants/alerts.ts b/x-pack/plugins/uptime/common/constants/alerts.ts index a259fc0a3eb81..61a7a02bf8b30 100644 --- a/x-pack/plugins/uptime/common/constants/alerts.ts +++ b/x-pack/plugins/uptime/common/constants/alerts.ts @@ -20,9 +20,14 @@ export const ACTION_GROUP_DEFINITIONS: ActionGroupDefinitions = { id: 'xpack.uptime.alerts.actionGroups.tls', name: 'Uptime TLS Alert', }, + DURATION_ANOMALY: { + id: 'xpack.uptime.alerts.actionGroups.durationAnomaly', + name: 'Uptime Duration Anomaly', + }, }; export const CLIENT_ALERT_TYPES = { MONITOR_STATUS: 'xpack.uptime.alerts.monitorStatus', TLS: 'xpack.uptime.alerts.tls', + DURATION_ANOMALY: 'xpack.uptime.alerts.durationAnomaly', }; diff --git a/x-pack/plugins/uptime/common/constants/rest_api.ts b/x-pack/plugins/uptime/common/constants/rest_api.ts index 169d175f02d3b..f3f06f776260d 100644 --- a/x-pack/plugins/uptime/common/constants/rest_api.ts +++ b/x-pack/plugins/uptime/common/constants/rest_api.ts @@ -24,4 +24,6 @@ export enum API_URLS { ML_DELETE_JOB = `/api/ml/jobs/delete_jobs`, ML_CAPABILITIES = '/api/ml/ml_capabilities', ML_ANOMALIES_RESULT = `/api/ml/results/anomalies_table_data`, + ALERT = '/api/alerts/alert/', + ALERTS_FIND = '/api/alerts/_find', } diff --git a/x-pack/plugins/uptime/public/state/api/__tests__/ml_anomaly.test.ts b/x-pack/plugins/uptime/common/lib/__tests__/ml.test.ts similarity index 95% rename from x-pack/plugins/uptime/public/state/api/__tests__/ml_anomaly.test.ts rename to x-pack/plugins/uptime/common/lib/__tests__/ml.test.ts index 838e5b8246b4b..122755638db7f 100644 --- a/x-pack/plugins/uptime/public/state/api/__tests__/ml_anomaly.test.ts +++ b/x-pack/plugins/uptime/common/lib/__tests__/ml.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getMLJobId } from '../ml_anomaly'; +import { getMLJobId } from '../ml'; describe('ML Anomaly API', () => { it('it generates a lowercase job id', async () => { diff --git a/x-pack/plugins/uptime/common/lib/index.ts b/x-pack/plugins/uptime/common/lib/index.ts index 2daec0adf87e4..33fe5b80d469b 100644 --- a/x-pack/plugins/uptime/common/lib/index.ts +++ b/x-pack/plugins/uptime/common/lib/index.ts @@ -6,3 +6,5 @@ export * from './combine_filters_and_user_search'; export * from './stringify_kueries'; + +export { getMLJobId } from './ml'; diff --git a/x-pack/plugins/uptime/common/lib/ml.ts b/x-pack/plugins/uptime/common/lib/ml.ts new file mode 100644 index 0000000000000..8be7c472fa5b9 --- /dev/null +++ b/x-pack/plugins/uptime/common/lib/ml.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ML_JOB_ID } from '../constants'; + +export const getJobPrefix = (monitorId: string) => { + // ML App doesn't support upper case characters in job name + // Also Spaces and the characters / ? , " < > | * are not allowed + // so we will replace all special chars with _ + + const prefix = monitorId.replace(/[^A-Z0-9]+/gi, '_').toLowerCase(); + + // ML Job ID can't be greater than 64 length, so will be substring it, and hope + // At such big length, there is minimum chance of having duplicate monitor id + // Subtracting ML_JOB_ID constant as well + const postfix = '_' + ML_JOB_ID; + + if ((prefix + postfix).length > 64) { + return prefix.substring(0, 64 - postfix.length) + '_'; + } + return prefix + '_'; +}; + +export const getMLJobId = (monitorId: string) => `${getJobPrefix(monitorId)}${ML_JOB_ID}`; diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json index a057e546e4414..f2b028e323ff6 100644 --- a/x-pack/plugins/uptime/kibana.json +++ b/x-pack/plugins/uptime/kibana.json @@ -2,7 +2,7 @@ "configPath": ["xpack", "uptime"], "id": "uptime", "kibanaVersion": "kibana", - "optionalPlugins": ["capabilities", "data", "home", "observability"], + "optionalPlugins": ["capabilities", "data", "home", "observability", "ml"], "requiredPlugins": [ "alerts", "embeddable", diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_manage_job.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_manage_job.test.tsx index 30038b030be56..841c577a4014b 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_manage_job.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_manage_job.test.tsx @@ -11,8 +11,8 @@ import { renderWithRouter, shallowWithRouter } from '../../../../lib'; describe('Manage ML Job', () => { it('shallow renders without errors', () => { - const spy = jest.spyOn(redux, 'useSelector'); - spy.mockReturnValue(true); + jest.spyOn(redux, 'useSelector').mockReturnValue(true); + jest.spyOn(redux, 'useDispatch').mockReturnValue(jest.fn()); const wrapper = shallowWithRouter( @@ -21,8 +21,8 @@ describe('Manage ML Job', () => { }); it('renders without errors', () => { - const spy = jest.spyOn(redux, 'useSelector'); - spy.mockReturnValue(true); + jest.spyOn(redux, 'useDispatch').mockReturnValue(jest.fn()); + jest.spyOn(redux, 'useSelector').mockReturnValue(true); const wrapper = renderWithRouter( diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/confirm_alert_delete.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/confirm_alert_delete.tsx new file mode 100644 index 0000000000000..cd5e509e3ad88 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/ml/confirm_alert_delete.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import * as labels from './translations'; + +interface Props { + onConfirm: () => void; + onCancel: () => void; +} + +export const ConfirmAlertDeletion: React.FC = ({ onConfirm, onCancel }) => { + return ( + + +

+ +

+
+
+ ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx index 248ea179ccd2b..5c3674761af84 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx @@ -7,7 +7,8 @@ import React, { useContext, useState } from 'react'; import { EuiButtonEmpty, EuiContextMenu, EuiIcon, EuiPopover } from '@elastic/eui'; -import { useSelector } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; +import { CLIENT_ALERT_TYPES } from '../../../../common/constants'; import { canDeleteMLJobSelector, hasMLJobSelector, @@ -18,6 +19,10 @@ import * as labels from './translations'; import { getMLJobLinkHref } from './ml_job_link'; import { useGetUrlParams } from '../../../hooks'; import { useMonitorId } from '../../../hooks'; +import { setAlertFlyoutType, setAlertFlyoutVisible } from '../../../state/actions'; +import { useAnomalyAlert } from './use_anomaly_alert'; +import { ConfirmAlertDeletion } from './confirm_alert_delete'; +import { deleteAlertAction } from '../../../state/actions/alerts'; interface Props { hasMLJob: boolean; @@ -40,6 +45,15 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro const monitorId = useMonitorId(); + const dispatch = useDispatch(); + + const anomalyAlert = useAnomalyAlert(); + + const [isConfirmAlertDeleteOpen, setIsConfirmAlertDeleteOpen] = useState(false); + + const deleteAnomalyAlert = () => + dispatch(deleteAlertAction.get({ alertId: anomalyAlert?.id as string })); + const button = ( , + onClick: () => { + if (anomalyAlert) { + setIsConfirmAlertDeleteOpen(true); + } else { + dispatch(setAlertFlyoutType(CLIENT_ALERT_TYPES.DURATION_ANOMALY)); + dispatch(setAlertFlyoutVisible(true)); + } + }, + }, { name: labels.DISABLE_ANOMALY_DETECTION, 'data-test-subj': 'uptimeDeleteMLJobBtn', @@ -82,12 +111,29 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro ]; return ( - setIsPopOverOpen(false)}> - - + <> + setIsPopOverOpen(false)} + > + + + {isConfirmAlertDeleteOpen && ( + { + deleteAnomalyAlert(); + setIsConfirmAlertDeleteOpen(false); + }} + onCancel={() => { + setIsConfirmAlertDeleteOpen(false); + }} + /> + )} + ); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx index e4bb3d0ac9e17..84634f328621f 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx @@ -13,59 +13,61 @@ import { isMLJobCreatingSelector, selectDynamicSettings, } from '../../../state/selectors'; -import { createMLJobAction, getExistingMLJobAction } from '../../../state/actions'; +import { + createMLJobAction, + getExistingMLJobAction, + setAlertFlyoutType, + setAlertFlyoutVisible, +} from '../../../state/actions'; import { MLJobLink } from './ml_job_link'; import * as labels from './translations'; -import { - useKibana, - KibanaReactNotifications, -} from '../../../../../../../src/plugins/kibana_react/public'; import { MLFlyoutView } from './ml_flyout'; -import { ML_JOB_ID } from '../../../../common/constants'; +import { CLIENT_ALERT_TYPES, ML_JOB_ID } from '../../../../common/constants'; import { UptimeRefreshContext, UptimeSettingsContext } from '../../../contexts'; import { useGetUrlParams } from '../../../hooks'; import { getDynamicSettings } from '../../../state/actions/dynamic_settings'; import { useMonitorId } from '../../../hooks'; +import { kibanaService } from '../../../state/kibana_service'; +import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; interface Props { onClose: () => void; } const showMLJobNotification = ( - notifications: KibanaReactNotifications, monitorId: string, basePath: string, range: { to: string; from: string }, success: boolean, - message = '' + error?: Error ) => { if (success) { - notifications.toasts.success({ - title: ( -

{labels.JOB_CREATED_SUCCESS_TITLE}

- ), - body: ( -

- {labels.JOB_CREATED_SUCCESS_MESSAGE} - - {labels.VIEW_JOB} - -

- ), - toastLifeTimeMs: 10000, - }); + kibanaService.toasts.addSuccess( + { + title: toMountPoint( +

{labels.JOB_CREATED_SUCCESS_TITLE}

+ ), + text: toMountPoint( +

+ {labels.JOB_CREATED_SUCCESS_MESSAGE} + + {labels.VIEW_JOB} + +

+ ), + }, + { toastLifeTimeMs: 10000 } + ); } else { - notifications.toasts.danger({ - title:

{labels.JOB_CREATION_FAILED}

, - body: message ??

{labels.JOB_CREATION_FAILED_MESSAGE}

, + kibanaService.toasts.addError(error!, { + title: labels.JOB_CREATION_FAILED, + toastMessage: labels.JOB_CREATION_FAILED_MESSAGE, toastLifeTimeMs: 10000, }); } }; export const MachineLearningFlyout: React.FC = ({ onClose }) => { - const { notifications } = useKibana(); - const dispatch = useDispatch(); const { data: hasMLJob, error } = useSelector(hasNewMLJobSelector); const isMLJobCreating = useSelector(isMLJobCreatingSelector); @@ -100,7 +102,6 @@ export const MachineLearningFlyout: React.FC = ({ onClose }) => { if (isCreatingJob && !isMLJobCreating) { if (hasMLJob) { showMLJobNotification( - notifications, monitorId as string, basePath, { to: dateRangeEnd, from: dateRangeStart }, @@ -112,31 +113,22 @@ export const MachineLearningFlyout: React.FC = ({ onClose }) => { loadMLJob(ML_JOB_ID); refreshApp(); + dispatch(setAlertFlyoutType(CLIENT_ALERT_TYPES.DURATION_ANOMALY)); + dispatch(setAlertFlyoutVisible(true)); } else { showMLJobNotification( - notifications, monitorId as string, basePath, { to: dateRangeEnd, from: dateRangeStart }, false, - error?.message || error?.body?.message + error as Error ); } setIsCreatingJob(false); onClose(); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - hasMLJob, - notifications, - onClose, - isCreatingJob, - error, - isMLJobCreating, - monitorId, - dispatch, - basePath, - ]); + }, [hasMLJob, onClose, isCreatingJob, error, isMLJobCreating, monitorId, dispatch, basePath]); useEffect(() => { if (hasExistingMLJob && !isMLJobCreating && !hasMLJob && heartbeatIndices) { diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx index 1de19dda3b88f..aa67c7ba1c2f9 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx @@ -16,12 +16,12 @@ import { import { deleteMLJobAction, getExistingMLJobAction, resetMLState } from '../../../state/actions'; import { ConfirmJobDeletion } from './confirm_delete'; import { UptimeRefreshContext } from '../../../contexts'; -import { getMLJobId } from '../../../state/api/ml_anomaly'; import * as labels from './translations'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ManageMLJobComponent } from './manage_ml_job'; import { JobStat } from '../../../../../../plugins/ml/public'; import { useMonitorId } from '../../../hooks'; +import { getMLJobId } from '../../../../common/lib'; export const MLIntegrationComponent = () => { const [isMlFlyoutOpen, setIsMlFlyoutOpen] = useState(false); diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/ml_job_link.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_job_link.tsx index 4b6f7e3ba061d..adc05695b4379 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/ml_job_link.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/ml_job_link.tsx @@ -8,7 +8,7 @@ import React from 'react'; import url from 'url'; import { EuiButtonEmpty } from '@elastic/eui'; import rison, { RisonValue } from 'rison-node'; -import { getMLJobId } from '../../../state/api/ml_anomaly'; +import { getMLJobId } from '../../../../common/lib'; interface Props { monitorId: string; diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx index bcc3fca770652..90ebdf10a73f5 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx @@ -89,6 +89,20 @@ export const DISABLE_ANOMALY_DETECTION = i18n.translate( } ); +export const ENABLE_ANOMALY_ALERT = i18n.translate( + 'xpack.uptime.ml.enableAnomalyDetectionPanel.enableAnomalyAlert', + { + defaultMessage: 'Enable anomaly alert', + } +); + +export const DISABLE_ANOMALY_ALERT = i18n.translate( + 'xpack.uptime.ml.enableAnomalyDetectionPanel.disableAnomalyAlert', + { + defaultMessage: 'Disable anomaly alert', + } +); + export const MANAGE_ANOMALY_DETECTION = i18n.translate( 'xpack.uptime.ml.enableAnomalyDetectionPanel.manageAnomalyDetectionTitle', { diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/use_anomaly_alert.ts b/x-pack/plugins/uptime/public/components/monitor/ml/use_anomaly_alert.ts new file mode 100644 index 0000000000000..d204cdf10012a --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/ml/use_anomaly_alert.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useContext, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { getExistingAlertAction } from '../../../state/actions/alerts'; +import { alertSelector, selectAlertFlyoutVisibility } from '../../../state/selectors'; +import { UptimeRefreshContext } from '../../../contexts'; +import { useMonitorId } from '../../../hooks'; + +export const useAnomalyAlert = () => { + const { lastRefresh } = useContext(UptimeRefreshContext); + + const dispatch = useDispatch(); + + const monitorId = useMonitorId(); + + const { data: anomalyAlert } = useSelector(alertSelector); + + const alertFlyoutVisible = useSelector(selectAlertFlyoutVisibility); + + useEffect(() => { + dispatch(getExistingAlertAction.get({ monitorId })); + }, [monitorId, lastRefresh, dispatch, alertFlyoutVisible]); + + return anomalyAlert; +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx index df8ceed76b796..29edb69f4674b 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx @@ -19,10 +19,10 @@ import { selectDurationLines, } from '../../../state/selectors'; import { UptimeRefreshContext } from '../../../contexts'; -import { getMLJobId } from '../../../state/api/ml_anomaly'; import { JobStat } from '../../../../../ml/public'; import { MonitorDurationComponent } from './monitor_duration'; import { MonitorIdParam } from '../../../../common/types'; +import { getMLJobId } from '../../../../common/lib'; export const MonitorDuration: React.FC = ({ monitorId }) => { const { diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/alert_expression_popover.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alert_expression_popover.tsx index 0ae8c3a93da94..b5ef240e67dbf 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/alert_expression_popover.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/alert_expression_popover.tsx @@ -14,8 +14,8 @@ interface AlertExpressionPopoverProps { 'data-test-subj': string; isEnabled?: boolean; id: string; + value: string | JSX.Element; isInvalid?: boolean; - value: string; } const getColor = ( diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/anomaly_alert.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/anomaly_alert.tsx new file mode 100644 index 0000000000000..4b84012575ae9 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/anomaly_alert.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiExpression, + EuiFlexItem, + EuiFlexGroup, + EuiSpacer, + EuiHealth, + EuiText, +} from '@elastic/eui'; +import { useSelector } from 'react-redux'; +import React, { useEffect, useState } from 'react'; +import { AnomalyTranslations } from './translations'; +import { AlertExpressionPopover } from '../alert_expression_popover'; +import { DEFAULT_SEVERITY, SelectSeverity } from './select_severity'; +import { monitorIdSelector } from '../../../../state/selectors'; +import { getSeverityColor, getSeverityType } from '../../../../../../ml/public'; + +interface Props { + alertParams: { [key: string]: any }; + setAlertParams: (key: string, value: any) => void; +} + +// eslint-disable-next-line import/no-default-export +export default function AnomalyAlertComponent({ setAlertParams, alertParams }: Props) { + const [severity, setSeverity] = useState(DEFAULT_SEVERITY); + + const monitorIdStore = useSelector(monitorIdSelector); + + const monitorId = monitorIdStore || alertParams?.monitorId; + + useEffect(() => { + setAlertParams('monitorId', monitorId); + }, [monitorId, setAlertParams]); + + useEffect(() => { + setAlertParams('severity', severity.val); + }, [severity, setAlertParams]); + + return ( + <> + + + + +
{monitorId}
+ + } + /> +
+ + + } + data-test-subj={'uptimeAnomalySeverity'} + description={AnomalyTranslations.hasAnomalyWithSeverity} + id="severity" + value={ + + {getSeverityType(severity.val)} + + } + isEnabled={true} + /> + +
+ + + ); +} diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/select_severity.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/select_severity.tsx new file mode 100644 index 0000000000000..0932d0c6eca8d --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/select_severity.tsx @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC, useState, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { EuiHealth, EuiSpacer, EuiSuperSelect, EuiText } from '@elastic/eui'; +import { getSeverityColor } from '../../../../../../ml/public'; + +const warningLabel = i18n.translate('xpack.uptime.controls.selectSeverity.warningLabel', { + defaultMessage: 'warning', +}); +const minorLabel = i18n.translate('xpack.uptime.controls.selectSeverity.minorLabel', { + defaultMessage: 'minor', +}); +const majorLabel = i18n.translate('xpack.uptime.controls.selectSeverity.majorLabel', { + defaultMessage: 'major', +}); +const criticalLabel = i18n.translate('xpack.uptime.controls.selectSeverity.criticalLabel', { + defaultMessage: 'critical', +}); + +const optionsMap = { + [warningLabel]: 0, + [minorLabel]: 25, + [majorLabel]: 50, + [criticalLabel]: 75, +}; + +interface TableSeverity { + val: number; + display: string; + color: string; +} + +export const SEVERITY_OPTIONS: TableSeverity[] = [ + { + val: 0, + display: warningLabel, + color: getSeverityColor(0), + }, + { + val: 25, + display: minorLabel, + color: getSeverityColor(25), + }, + { + val: 50, + display: majorLabel, + color: getSeverityColor(50), + }, + { + val: 75, + display: criticalLabel, + color: getSeverityColor(75), + }, +]; + +function optionValueToThreshold(value: number) { + // Get corresponding threshold object with required display and val properties from the specified value. + let threshold = SEVERITY_OPTIONS.find((opt) => opt.val === value); + + // Default to warning if supplied value doesn't map to one of the options. + if (threshold === undefined) { + threshold = SEVERITY_OPTIONS[0]; + } + + return threshold; +} + +export const DEFAULT_SEVERITY = SEVERITY_OPTIONS[3]; + +const getSeverityOptions = () => + SEVERITY_OPTIONS.map(({ color, display, val }) => ({ + 'data-test-subj': `alertAnomaly${display}`, + value: display, + inputDisplay: ( + + + {display} + + + ), + dropdownDisplay: ( + + + {display} + + + +

+ +

+
+
+ ), + })); + +interface Props { + onChange: (sev: TableSeverity) => void; + value: TableSeverity; +} + +export const SelectSeverity: FC = ({ onChange, value }) => { + const [severity, setSeverity] = useState(DEFAULT_SEVERITY); + + const onSeverityChange = (valueDisplay: string) => { + const option = optionValueToThreshold(optionsMap[valueDisplay]); + setSeverity(option); + onChange(option); + }; + + useEffect(() => { + setSeverity(value); + }, [value]); + + return ( + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/translations.ts b/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/translations.ts new file mode 100644 index 0000000000000..5fd37609f86bf --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/translations.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const AnomalyTranslations = { + criteriaAriaLabel: i18n.translate('xpack.uptime.alerts.anomaly.criteriaExpression.ariaLabel', { + defaultMessage: 'An expression displaying the criteria for a selected monitor.', + }), + whenMonitor: i18n.translate('xpack.uptime.alerts.anomaly.criteriaExpression.description', { + defaultMessage: 'When monitor', + }), + scoreAriaLabel: i18n.translate('xpack.uptime.alerts.anomaly.scoreExpression.ariaLabel', { + defaultMessage: 'An expression displaying the criteria for an anomaly alert threshold.', + }), + hasAnomalyWithSeverity: i18n.translate( + 'xpack.uptime.alerts.anomaly.scoreExpression.description', + { + defaultMessage: 'has anomaly with severity', + description: 'An expression displaying the criteria for an anomaly alert threshold.', + } + ), +}; diff --git a/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx b/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx new file mode 100644 index 0000000000000..f0eb305461582 --- /dev/null +++ b/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Provider as ReduxProvider } from 'react-redux'; +import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; +import { CLIENT_ALERT_TYPES } from '../../../common/constants'; +import { DurationAnomalyTranslations } from './translations'; +import { AlertTypeInitializer } from '.'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; +import { store } from '../../state'; + +const { name, defaultActionMessage } = DurationAnomalyTranslations; +const AnomalyAlertExpression = React.lazy(() => + import('../../components/overview/alerts/anomaly_alert/anomaly_alert') +); +export const initDurationAnomalyAlertType: AlertTypeInitializer = ({ + core, + plugins, +}): AlertTypeModel => ({ + id: CLIENT_ALERT_TYPES.DURATION_ANOMALY, + iconClass: 'uptimeApp', + alertParamsExpression: (params: any) => ( + + + + + + ), + name, + validate: () => ({ errors: {} }), + defaultActionMessage, + requiresAppContext: false, +}); diff --git a/x-pack/plugins/uptime/public/lib/alert_types/index.ts b/x-pack/plugins/uptime/public/lib/alert_types/index.ts index f2f72311d2262..5eb693c6bd5c3 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/index.ts +++ b/x-pack/plugins/uptime/public/lib/alert_types/index.ts @@ -9,6 +9,7 @@ import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; import { initMonitorStatusAlertType } from './monitor_status'; import { initTlsAlertType } from './tls'; import { ClientPluginsStart } from '../../apps/plugin'; +import { initDurationAnomalyAlertType } from './duration_anomaly'; export type AlertTypeInitializer = (dependenies: { core: CoreStart; @@ -18,4 +19,5 @@ export type AlertTypeInitializer = (dependenies: { export const alertTypeInitializers: AlertTypeInitializer[] = [ initMonitorStatusAlertType, initTlsAlertType, + initDurationAnomalyAlertType, ]; diff --git a/x-pack/plugins/uptime/public/lib/alert_types/translations.ts b/x-pack/plugins/uptime/public/lib/alert_types/translations.ts index 11fa70bc56f4a..9232dd590ad5e 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/translations.ts +++ b/x-pack/plugins/uptime/public/lib/alert_types/translations.ts @@ -26,7 +26,7 @@ export const TlsTranslations = { {expiringConditionalOpen} Expiring cert count: {expiringCount} Expiring Certificates: {expiringCommonNameAndDate} -{expiringConditionalClose} +{expiringConditionalClose} {agingConditionalOpen} Aging cert count: {agingCount} @@ -49,3 +49,23 @@ Aging Certificates: {agingCommonNameAndDate} defaultMessage: 'Uptime TLS', }), }; + +export const DurationAnomalyTranslations = { + defaultActionMessage: i18n.translate('xpack.uptime.alerts.durationAnomaly.defaultActionMessage', { + defaultMessage: `Abnormal ({severity} level) response time detected on {monitor} with url {monitorUrl} at {anomalyStartTimestamp}. Anomaly severity score is {severityScore}. +Response times as high as {slowestAnomalyResponse} have been detected from location {observerLocation}. Expected response time is {expectedResponseTime}.`, + values: { + severity: '{{state.severity}}', + anomalyStartTimestamp: '{{state.anomalyStartTimestamp}}', + monitor: '{{state.monitor}}', + monitorUrl: '{{{state.monitorUrl}}}', + slowestAnomalyResponse: '{{state.slowestAnomalyResponse}}', + expectedResponseTime: '{{state.expectedResponseTime}}', + severityScore: '{{state.severityScore}}', + observerLocation: '{{state.observerLocation}}', + }, + }), + name: i18n.translate('xpack.uptime.alerts.durationAnomaly.clientName', { + defaultMessage: 'Uptime Duration Anomaly', + }), +}; diff --git a/x-pack/plugins/uptime/public/pages/monitor.tsx b/x-pack/plugins/uptime/public/pages/monitor.tsx index ab7cf5b2cb3e2..f7012fc5119e9 100644 --- a/x-pack/plugins/uptime/public/pages/monitor.tsx +++ b/x-pack/plugins/uptime/public/pages/monitor.tsx @@ -16,6 +16,7 @@ import { MonitorCharts } from '../components/monitor'; import { MonitorStatusDetails, PingList } from '../components/monitor'; import { getDynamicSettings } from '../state/actions/dynamic_settings'; import { Ping } from '../../common/runtime_types/ping'; +import { setSelectedMonitorId } from '../state/actions'; const isAutogeneratedId = (id: string) => { const autoGeneratedId = /^auto-(icmp|http|tcp)-OX[A-F0-9]{16}-[a-f0-9]{16}/; @@ -43,6 +44,10 @@ export const MonitorPage: React.FC = () => { const monitorId = useMonitorId(); + useEffect(() => { + dispatch(setSelectedMonitorId(monitorId)); + }, [monitorId, dispatch]); + const selectedMonitor = useSelector(monitorStatusSelector); useTrackPageview({ app: 'uptime', path: 'monitor' }); diff --git a/x-pack/plugins/uptime/public/state/actions/alerts.ts b/x-pack/plugins/uptime/public/state/actions/alerts.ts new file mode 100644 index 0000000000000..a650a9ba8d08b --- /dev/null +++ b/x-pack/plugins/uptime/public/state/actions/alerts.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createAsyncAction } from './utils'; +import { MonitorIdParam } from './types'; +import { Alert } from '../../../../triggers_actions_ui/public'; + +export const getExistingAlertAction = createAsyncAction( + 'GET EXISTING ALERTS' +); + +export const deleteAlertAction = createAsyncAction<{ alertId: string }, any>('DELETE ALERTS'); diff --git a/x-pack/plugins/uptime/public/state/actions/ui.ts b/x-pack/plugins/uptime/public/state/actions/ui.ts index 04ad6c2fa0bf3..9387506e4e7b5 100644 --- a/x-pack/plugins/uptime/public/state/actions/ui.ts +++ b/x-pack/plugins/uptime/public/state/actions/ui.ts @@ -25,3 +25,5 @@ export const setSearchTextAction = createAction('SET SEARCH'); export const toggleIntegrationsPopover = createAction( 'TOGGLE INTEGRATION POPOVER STATE' ); + +export const setSelectedMonitorId = createAction('SET MONITOR ID'); diff --git a/x-pack/plugins/uptime/public/state/api/alerts.ts b/x-pack/plugins/uptime/public/state/api/alerts.ts new file mode 100644 index 0000000000000..526abd6b303e5 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/api/alerts.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { apiService } from './utils'; +import { API_URLS } from '../../../common/constants'; +import { MonitorIdParam } from '../actions/types'; +import { Alert } from '../../../../triggers_actions_ui/public'; + +export const fetchAlertRecords = async ({ monitorId }: MonitorIdParam): Promise => { + const data = { + page: 1, + per_page: 500, + filter: 'alert.attributes.alertTypeId:(xpack.uptime.alerts.durationAnomaly)', + default_search_operator: 'AND', + sort_field: 'name.keyword', + sort_order: 'asc', + }; + const alerts = await apiService.get(API_URLS.ALERTS_FIND, data); + return alerts.data.find((alert: Alert) => alert.params.monitorId === monitorId); +}; + +export const disableAnomalyAlert = async ({ alertId }: { alertId: string }) => { + return await apiService.delete(API_URLS.ALERT + alertId); +}; diff --git a/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts b/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts index 5ec7a6262db66..1d25f35e8f38a 100644 --- a/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts +++ b/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts @@ -7,38 +7,19 @@ import moment from 'moment'; import { apiService } from './utils'; import { AnomalyRecords, AnomalyRecordsParams } from '../actions'; -import { API_URLS, ML_JOB_ID, ML_MODULE_ID } from '../../../common/constants'; +import { API_URLS, ML_MODULE_ID } from '../../../common/constants'; import { - MlCapabilitiesResponse, DataRecognizerConfigResponse, JobExistResult, + MlCapabilitiesResponse, } from '../../../../../plugins/ml/public'; import { CreateMLJobSuccess, DeleteJobResults, - MonitorIdParam, HeartbeatIndicesParam, + MonitorIdParam, } from '../actions/types'; - -const getJobPrefix = (monitorId: string) => { - // ML App doesn't support upper case characters in job name - // Also Spaces and the characters / ? , " < > | * are not allowed - // so we will replace all special chars with _ - - const prefix = monitorId.replace(/[^A-Z0-9]+/gi, '_').toLowerCase(); - - // ML Job ID can't be greater than 64 length, so will be substring it, and hope - // At such big length, there is minimum chance of having duplicate monitor id - // Subtracting ML_JOB_ID constant as well - const postfix = '_' + ML_JOB_ID; - - if ((prefix + postfix).length > 64) { - return prefix.substring(0, 64 - postfix.length) + '_'; - } - return prefix + '_'; -}; - -export const getMLJobId = (monitorId: string) => `${getJobPrefix(monitorId)}${ML_JOB_ID}`; +import { getJobPrefix, getMLJobId } from '../../../common/lib/ml'; export const getMLCapabilities = async (): Promise => { return await apiService.get(API_URLS.ML_CAPABILITIES); diff --git a/x-pack/plugins/uptime/public/state/effects/alerts.ts b/x-pack/plugins/uptime/public/state/effects/alerts.ts new file mode 100644 index 0000000000000..5f71b0bea7b2c --- /dev/null +++ b/x-pack/plugins/uptime/public/state/effects/alerts.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action } from 'redux-actions'; +import { call, put, takeLatest, select } from 'redux-saga/effects'; +import { fetchEffectFactory } from './fetch_effect'; +import { deleteAlertAction, getExistingAlertAction } from '../actions/alerts'; +import { disableAnomalyAlert, fetchAlertRecords } from '../api/alerts'; +import { kibanaService } from '../kibana_service'; +import { monitorIdSelector } from '../selectors'; + +export function* fetchAlertsEffect() { + yield takeLatest( + getExistingAlertAction.get, + fetchEffectFactory( + fetchAlertRecords, + getExistingAlertAction.success, + getExistingAlertAction.fail + ) + ); + + yield takeLatest(String(deleteAlertAction.get), function* (action: Action<{ alertId: string }>) { + try { + const response = yield call(disableAnomalyAlert, action.payload); + yield put(deleteAlertAction.success(response)); + kibanaService.core.notifications.toasts.addSuccess('Alert successfully deleted!'); + const monitorId = yield select(monitorIdSelector); + yield put(getExistingAlertAction.get({ monitorId })); + } catch (err) { + kibanaService.core.notifications.toasts.addError(err, { + title: 'Alert cannot be deleted', + }); + yield put(deleteAlertAction.fail(err)); + } + }); +} diff --git a/x-pack/plugins/uptime/public/state/effects/index.ts b/x-pack/plugins/uptime/public/state/effects/index.ts index 211067c840d54..b13ba7f1a9107 100644 --- a/x-pack/plugins/uptime/public/state/effects/index.ts +++ b/x-pack/plugins/uptime/public/state/effects/index.ts @@ -17,6 +17,7 @@ import { fetchMonitorDurationEffect } from './monitor_duration'; import { fetchMLJobEffect } from './ml_anomaly'; import { fetchIndexStatusEffect } from './index_status'; import { fetchCertificatesEffect } from '../certificates/certificates'; +import { fetchAlertsEffect } from './alerts'; export function* rootEffect() { yield fork(fetchMonitorDetailsEffect); @@ -33,4 +34,5 @@ export function* rootEffect() { yield fork(fetchMonitorDurationEffect); yield fork(fetchIndexStatusEffect); yield fork(fetchCertificatesEffect); + yield fork(fetchAlertsEffect); } diff --git a/x-pack/plugins/uptime/public/state/effects/ml_anomaly.ts b/x-pack/plugins/uptime/public/state/effects/ml_anomaly.ts index a6a376b546ab8..00f8a388c689f 100644 --- a/x-pack/plugins/uptime/public/state/effects/ml_anomaly.ts +++ b/x-pack/plugins/uptime/public/state/effects/ml_anomaly.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { takeLatest } from 'redux-saga/effects'; +import { Action } from 'redux-actions'; +import { call, put, select, takeLatest } from 'redux-saga/effects'; import { getMLCapabilitiesAction, getExistingMLJobAction, @@ -20,6 +21,9 @@ import { deleteMLJob, getMLCapabilities, } from '../api/ml_anomaly'; +import { deleteAlertAction } from '../actions/alerts'; +import { alertSelector } from '../selectors'; +import { MonitorIdParam } from '../actions/types'; export function* fetchMLJobEffect() { yield takeLatest( @@ -38,10 +42,22 @@ export function* fetchMLJobEffect() { getAnomalyRecordsAction.fail ) ); - yield takeLatest( - deleteMLJobAction.get, - fetchEffectFactory(deleteMLJob, deleteMLJobAction.success, deleteMLJobAction.fail) - ); + + yield takeLatest(String(deleteMLJobAction.get), function* (action: Action) { + try { + const response = yield call(deleteMLJob, action.payload); + yield put(deleteMLJobAction.success(response)); + + // let's delete alert as well if it's there + const { data: anomalyAlert } = yield select(alertSelector); + if (anomalyAlert) { + yield put(deleteAlertAction.get({ alertId: anomalyAlert.id as string })); + } + } catch (err) { + yield put(deleteMLJobAction.fail(err)); + } + }); + yield takeLatest( getMLCapabilitiesAction.get, fetchEffectFactory( diff --git a/x-pack/plugins/uptime/public/state/kibana_service.ts b/x-pack/plugins/uptime/public/state/kibana_service.ts index 4fd2d446daa17..f1eb3af9da667 100644 --- a/x-pack/plugins/uptime/public/state/kibana_service.ts +++ b/x-pack/plugins/uptime/public/state/kibana_service.ts @@ -20,6 +20,10 @@ class KibanaService { apiService.http = this._core.http; } + public get toasts() { + return this._core.notifications.toasts; + } + private constructor() {} static getInstance(): KibanaService { diff --git a/x-pack/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap b/x-pack/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap index c11b146101d35..040fbf7f4fe0a 100644 --- a/x-pack/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap +++ b/x-pack/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap @@ -9,6 +9,7 @@ Object { "id": "popover-2", "open": true, }, + "monitorId": "test", "searchText": "", } `; @@ -19,6 +20,7 @@ Object { "basePath": "yyz", "esKuery": "", "integrationsPopoverOpen": null, + "monitorId": "test", "searchText": "", } `; diff --git a/x-pack/plugins/uptime/public/state/reducers/__tests__/ui.test.ts b/x-pack/plugins/uptime/public/state/reducers/__tests__/ui.test.ts index 4683c654270db..c265cd9fc7ecd 100644 --- a/x-pack/plugins/uptime/public/state/reducers/__tests__/ui.test.ts +++ b/x-pack/plugins/uptime/public/state/reducers/__tests__/ui.test.ts @@ -24,6 +24,7 @@ describe('ui reducer', () => { esKuery: '', integrationsPopoverOpen: null, searchText: '', + monitorId: 'test', }, action ) @@ -43,6 +44,7 @@ describe('ui reducer', () => { esKuery: '', integrationsPopoverOpen: null, searchText: '', + monitorId: 'test', }, action ) @@ -59,6 +61,7 @@ describe('ui reducer', () => { esKuery: '', integrationsPopoverOpen: null, searchText: '', + monitorId: 'test', }, action ) @@ -68,6 +71,7 @@ describe('ui reducer', () => { "basePath": "", "esKuery": "", "integrationsPopoverOpen": null, + "monitorId": "test", "searchText": "", } `); @@ -83,6 +87,7 @@ describe('ui reducer', () => { esKuery: '', integrationsPopoverOpen: null, searchText: '', + monitorId: 'test', }, action ) @@ -92,6 +97,7 @@ describe('ui reducer', () => { "basePath": "", "esKuery": "", "integrationsPopoverOpen": null, + "monitorId": "test", "searchText": "lorem ipsum", } `); diff --git a/x-pack/plugins/uptime/public/state/reducers/alerts.ts b/x-pack/plugins/uptime/public/state/reducers/alerts.ts new file mode 100644 index 0000000000000..a2cd844e24964 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/reducers/alerts.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { handleActions } from 'redux-actions'; +import { getAsyncInitialState, handleAsyncAction } from './utils'; +import { AsyncInitialState } from './types'; +import { deleteAlertAction, getExistingAlertAction } from '../actions/alerts'; +import { Alert } from '../../../../triggers_actions_ui/public'; + +export interface AlertsState { + alert: AsyncInitialState; + alertDeletion: AsyncInitialState; +} + +const initialState: AlertsState = { + alert: getAsyncInitialState(), + alertDeletion: getAsyncInitialState(), +}; + +export const alertsReducer = handleActions( + { + ...handleAsyncAction('alert', getExistingAlertAction), + ...handleAsyncAction('alertDeletion', deleteAlertAction), + }, + initialState +); diff --git a/x-pack/plugins/uptime/public/state/reducers/index.ts b/x-pack/plugins/uptime/public/state/reducers/index.ts index c05c740ab8ebf..01baf7cf07c92 100644 --- a/x-pack/plugins/uptime/public/state/reducers/index.ts +++ b/x-pack/plugins/uptime/public/state/reducers/index.ts @@ -20,6 +20,7 @@ import { indexStatusReducer } from './index_status'; import { mlJobsReducer } from './ml_anomaly'; import { certificatesReducer } from '../certificates/certificates'; import { selectedFiltersReducer } from './selected_filters'; +import { alertsReducer } from './alerts'; export const rootReducer = combineReducers({ monitor: monitorReducer, @@ -37,4 +38,5 @@ export const rootReducer = combineReducers({ indexStatus: indexStatusReducer, certificates: certificatesReducer, selectedFilters: selectedFiltersReducer, + alerts: alertsReducer, }); diff --git a/x-pack/plugins/uptime/public/state/reducers/ui.ts b/x-pack/plugins/uptime/public/state/reducers/ui.ts index 3cf4ae9c0bbf2..568234a3a83cd 100644 --- a/x-pack/plugins/uptime/public/state/reducers/ui.ts +++ b/x-pack/plugins/uptime/public/state/reducers/ui.ts @@ -14,6 +14,7 @@ import { setAlertFlyoutType, setAlertFlyoutVisible, setSearchTextAction, + setSelectedMonitorId, } from '../actions'; export interface UiState { @@ -23,6 +24,7 @@ export interface UiState { esKuery: string; searchText: string; integrationsPopoverOpen: PopoverState | null; + monitorId: string; } const initialState: UiState = { @@ -31,6 +33,7 @@ const initialState: UiState = { esKuery: '', searchText: '', integrationsPopoverOpen: null, + monitorId: '', }; export const uiReducer = handleActions( @@ -64,6 +67,10 @@ export const uiReducer = handleActions( ...state, searchText: action.payload, }), + [String(setSelectedMonitorId)]: (state, action: Action) => ({ + ...state, + monitorId: action.payload, + }), }, initialState ); diff --git a/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts b/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts index b1885ddeeba3f..de8615c7016a7 100644 --- a/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts +++ b/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts @@ -45,6 +45,7 @@ describe('state selectors', () => { esKuery: '', integrationsPopoverOpen: null, searchText: '', + monitorId: '', }, monitorStatus: { status: null, @@ -108,6 +109,10 @@ describe('state selectors', () => { }, }, selectedFilters: null, + alerts: { + alertDeletion: { data: null, loading: false }, + alert: { data: null, loading: false }, + }, }; it('selects base path from state', () => { diff --git a/x-pack/plugins/uptime/public/state/selectors/index.ts b/x-pack/plugins/uptime/public/state/selectors/index.ts index 4c2b671203f0a..bf6c9b3666a6a 100644 --- a/x-pack/plugins/uptime/public/state/selectors/index.ts +++ b/x-pack/plugins/uptime/public/state/selectors/index.ts @@ -59,6 +59,8 @@ export const hasNewMLJobSelector = ({ ml }: AppState) => ml.createJob; export const isMLJobCreatingSelector = ({ ml }: AppState) => ml.createJob.loading; export const isMLJobDeletingSelector = ({ ml }: AppState) => ml.deleteJob.loading; +export const isAnomalyAlertDeletingSelector = ({ alerts }: AppState) => + alerts.alertDeletion.loading; export const isMLJobDeletedSelector = ({ ml }: AppState) => ml.deleteJob; @@ -88,3 +90,7 @@ export const esKuerySelector = ({ ui: { esKuery } }: AppState) => esKuery; export const searchTextSelector = ({ ui: { searchText } }: AppState) => searchText; export const selectedFiltersSelector = ({ selectedFilters }: AppState) => selectedFilters; + +export const monitorIdSelector = ({ ui: { monitorId } }: AppState) => monitorId; + +export const alertSelector = ({ alerts }: AppState) => alerts.alert; diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts index 2e732f59e4f30..75d9c8aa959b1 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts @@ -14,6 +14,7 @@ import { import { UMKibanaRoute } from '../../../rest_api'; import { PluginSetupContract } from '../../../../../features/server'; import { DynamicSettings } from '../../../../common/runtime_types'; +import { MlPluginSetup as MlSetup } from '../../../../../ml/server'; export type APICaller = ( endpoint: string, @@ -39,6 +40,7 @@ export interface UptimeCorePlugins { alerts: any; elasticsearch: any; usageCollection: UsageCollectionSetup; + ml: MlSetup; } export interface UMBackendFrameworkAdapter { diff --git a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts index d85752768b47b..a38132d0f7a83 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts @@ -17,7 +17,7 @@ import { GetMonitorStatusResult } from '../../requests'; import { AlertType } from '../../../../../alerts/server'; import { IRouter } from 'kibana/server'; import { UMServerLibs } from '../../lib'; -import { UptimeCoreSetup } from '../../adapters'; +import { UptimeCorePlugins, UptimeCoreSetup } from '../../adapters'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; @@ -33,9 +33,10 @@ const bootstrapDependencies = (customRequests?: any) => { // these server/libs parameters don't have any functionality, which is fine // because we aren't testing them here const server: UptimeCoreSetup = { router }; + const plugins: UptimeCorePlugins = {} as any; const libs: UMServerLibs = { requests: {} } as UMServerLibs; libs.requests = { ...libs.requests, ...customRequests }; - return { server, libs }; + return { server, libs, plugins }; }; /** @@ -82,8 +83,8 @@ describe('status check alert', () => { expect.assertions(4); const mockGetter = jest.fn(); mockGetter.mockReturnValue([]); - const { server, libs } = bootstrapDependencies({ getMonitorStatus: mockGetter }); - const alert = statusCheckAlertFactory(server, libs); + const { server, libs, plugins } = bootstrapDependencies({ getMonitorStatus: mockGetter }); + const alert = statusCheckAlertFactory(server, libs, plugins); // @ts-ignore the executor can return `void`, but ours never does const state: Record = await alert.executor(mockOptions()); @@ -128,8 +129,8 @@ describe('status check alert', () => { status: 'down', }, ]); - const { server, libs } = bootstrapDependencies({ getMonitorStatus: mockGetter }); - const alert = statusCheckAlertFactory(server, libs); + const { server, libs, plugins } = bootstrapDependencies({ getMonitorStatus: mockGetter }); + const alert = statusCheckAlertFactory(server, libs, plugins); const options = mockOptions(); const alertServices: AlertServicesMock = options.services; // @ts-ignore the executor can return `void`, but ours never does @@ -213,11 +214,11 @@ describe('status check alert', () => { status: 'down', }, ]); - const { server, libs } = bootstrapDependencies({ + const { server, libs, plugins } = bootstrapDependencies({ getMonitorStatus: mockGetter, getIndexPattern: jest.fn(), }); - const alert = statusCheckAlertFactory(server, libs); + const alert = statusCheckAlertFactory(server, libs, plugins); const options = mockOptions({ numTimes: 4, timerange: { from: 'now-14h', to: 'now' }, @@ -286,11 +287,11 @@ describe('status check alert', () => { status: 'down', }, ]); - const { server, libs } = bootstrapDependencies({ + const { server, libs, plugins } = bootstrapDependencies({ getMonitorStatus: mockGetter, getIndexPattern: jest.fn(), }); - const alert = statusCheckAlertFactory(server, libs); + const alert = statusCheckAlertFactory(server, libs, plugins); const options = mockOptions({ numTimes: 3, timerangeUnit: 'm', @@ -371,11 +372,11 @@ describe('status check alert', () => { toISOStringSpy.mockImplementation(() => 'search test'); const mockGetter = jest.fn(); mockGetter.mockReturnValue([]); - const { server, libs } = bootstrapDependencies({ + const { server, libs, plugins } = bootstrapDependencies({ getIndexPattern: jest.fn(), getMonitorStatus: mockGetter, }); - const alert = statusCheckAlertFactory(server, libs); + const alert = statusCheckAlertFactory(server, libs, plugins); const options = mockOptions({ numTimes: 20, timerangeCount: 30, @@ -467,12 +468,12 @@ describe('status check alert', () => { availabilityRatio: 0.909245845760545, }, ]); - const { server, libs } = bootstrapDependencies({ + const { server, libs, plugins } = bootstrapDependencies({ getMonitorAvailability: mockAvailability, getMonitorStatus: mockGetter, getIndexPattern: jest.fn(), }); - const alert = statusCheckAlertFactory(server, libs); + const alert = statusCheckAlertFactory(server, libs, plugins); const options = mockOptions({ availability: { range: 35, @@ -559,11 +560,11 @@ describe('status check alert', () => { mockGetter.mockReturnValue([]); const mockAvailability = jest.fn(); mockAvailability.mockReturnValue([]); - const { server, libs } = bootstrapDependencies({ + const { server, libs, plugins } = bootstrapDependencies({ getMonitorAvailability: mockAvailability, getIndexPattern: jest.fn(), }); - const alert = statusCheckAlertFactory(server, libs); + const alert = statusCheckAlertFactory(server, libs, plugins); const options = mockOptions({ availability: { range: 23, @@ -600,11 +601,11 @@ describe('status check alert', () => { mockGetter.mockReturnValue([]); const mockAvailability = jest.fn(); mockAvailability.mockReturnValue([]); - const { server, libs } = bootstrapDependencies({ + const { server, libs, plugins } = bootstrapDependencies({ getMonitorAvailability: mockAvailability, getIndexPattern: jest.fn(), }); - const alert = statusCheckAlertFactory(server, libs); + const alert = statusCheckAlertFactory(server, libs, plugins); const options = mockOptions({ availability: { range: 23, @@ -748,8 +749,8 @@ describe('status check alert', () => { let alert: AlertType; beforeEach(() => { - const { server, libs } = bootstrapDependencies(); - alert = statusCheckAlertFactory(server, libs); + const { server, libs, plugins } = bootstrapDependencies(); + alert = statusCheckAlertFactory(server, libs, plugins); }); it('creates an alert with expected params', () => { diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts new file mode 100644 index 0000000000000..7dd357e99b83d --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; +import { schema } from '@kbn/config-schema'; +import { ILegacyScopedClusterClient } from 'kibana/server'; +import { updateState } from './common'; +import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants'; +import { commonStateTranslations, durationAnomalyTranslations } from './translations'; +import { AnomaliesTableRecord } from '../../../../ml/common/types/anomalies'; +import { getSeverityType } from '../../../../ml/common/util/anomaly_utils'; +import { getLatestMonitor } from '../requests'; +import { savedObjectsAdapter } from '../saved_objects'; +import { UptimeCorePlugins } from '../adapters/framework'; +import { UptimeAlertTypeFactory } from './types'; +import { Ping } from '../../../common/runtime_types/ping'; +import { getMLJobId } from '../../../common/lib'; + +const { DURATION_ANOMALY } = ACTION_GROUP_DEFINITIONS; + +export const getAnomalySummary = (anomaly: AnomaliesTableRecord, monitorInfo: Ping) => { + return { + severity: getSeverityType(anomaly.severity), + severityScore: Math.round(anomaly.severity), + anomalyStartTimestamp: moment(anomaly.source.timestamp).toISOString(), + monitor: anomaly.source['monitor.id'], + monitorUrl: monitorInfo.url?.full, + slowestAnomalyResponse: Math.round(anomaly.actualSort / 1000) + ' ms', + expectedResponseTime: Math.round(anomaly.typicalSort / 1000) + ' ms', + observerLocation: anomaly.entityValue, + }; +}; + +const getAnomalies = async ( + plugins: UptimeCorePlugins, + mlClusterClient: ILegacyScopedClusterClient, + params: Record, + lastCheckedAt: string +) => { + const { getAnomaliesTableData } = plugins.ml.resultsServiceProvider(mlClusterClient, { + params: 'DummyKibanaRequest', + } as any); + + return await getAnomaliesTableData( + [getMLJobId(params.monitorId)], + [], + [], + 'auto', + params.severity, + moment(lastCheckedAt).valueOf(), + moment().valueOf(), + Intl.DateTimeFormat().resolvedOptions().timeZone, + 500, + 10, + undefined + ); +}; + +export const durationAnomalyAlertFactory: UptimeAlertTypeFactory = (_server, _libs, plugins) => ({ + id: 'xpack.uptime.alerts.durationAnomaly', + name: durationAnomalyTranslations.alertFactoryName, + validate: { + params: schema.object({ + monitorId: schema.string(), + severity: schema.number(), + }), + }, + defaultActionGroupId: DURATION_ANOMALY.id, + actionGroups: [ + { + id: DURATION_ANOMALY.id, + name: DURATION_ANOMALY.name, + }, + ], + actionVariables: { + context: [], + state: [...durationAnomalyTranslations.actionVariables, ...commonStateTranslations], + }, + producer: 'uptime', + async executor(options) { + const { + services: { + alertInstanceFactory, + callCluster, + savedObjectsClient, + getLegacyScopedClusterClient, + }, + state, + params, + } = options; + + const { anomalies } = + (await getAnomalies( + plugins, + getLegacyScopedClusterClient(plugins.ml.mlClient), + params, + state.lastCheckedAt + )) ?? {}; + + const foundAnomalies = anomalies?.length > 0; + + if (foundAnomalies) { + const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings( + savedObjectsClient + ); + const monitorInfo = await getLatestMonitor({ + dynamicSettings, + callES: callCluster, + dateStart: 'now-15m', + dateEnd: 'now', + monitorId: params.monitorId, + }); + anomalies.forEach((anomaly, index) => { + const alertInstance = alertInstanceFactory(DURATION_ANOMALY.id + index); + const summary = getAnomalySummary(anomaly, monitorInfo); + alertInstance.replaceState({ + ...updateState(state, false), + ...summary, + }); + alertInstance.scheduleActions(DURATION_ANOMALY.id); + }); + } + + return updateState(state, foundAnomalies); + }, +}); diff --git a/x-pack/plugins/uptime/server/lib/alerts/index.ts b/x-pack/plugins/uptime/server/lib/alerts/index.ts index 661df39ece628..c8d3037f98aeb 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/index.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/index.ts @@ -7,8 +7,10 @@ import { UptimeAlertTypeFactory } from './types'; import { statusCheckAlertFactory } from './status_check'; import { tlsAlertFactory } from './tls'; +import { durationAnomalyAlertFactory } from './duration_anomaly'; export const uptimeAlertTypeFactories: UptimeAlertTypeFactory[] = [ statusCheckAlertFactory, tlsAlertFactory, + durationAnomalyAlertFactory, ]; diff --git a/x-pack/plugins/uptime/server/lib/alerts/translations.ts b/x-pack/plugins/uptime/server/lib/alerts/translations.ts index e41930aad5af0..50eedcd4fa69e 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/translations.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/translations.ts @@ -148,3 +148,93 @@ export const tlsTranslations = { }, }), }; + +export const durationAnomalyTranslations = { + alertFactoryName: i18n.translate('xpack.uptime.alerts.durationAnomaly', { + defaultMessage: 'Uptime Duration Anomaly', + }), + actionVariables: [ + { + name: 'severity', + description: i18n.translate( + 'xpack.uptime.alerts.durationAnomaly.actionVariables.state.severity', + { + defaultMessage: 'The severity of the anomaly.', + } + ), + }, + { + name: 'anomalyStartTimestamp', + description: i18n.translate( + 'xpack.uptime.alerts.durationAnomaly.actionVariables.state.anomalyStartTimestamp', + { + defaultMessage: 'ISO8601 timestamp of the start of the anomaly.', + } + ), + }, + { + name: 'monitor', + description: i18n.translate( + 'xpack.uptime.alerts.durationAnomaly.actionVariables.state.monitor', + { + defaultMessage: + 'A human friendly rendering of name or ID, preferring name (e.g. My Monitor)', + } + ), + }, + { + name: 'monitorId', + description: i18n.translate( + 'xpack.uptime.alerts.durationAnomaly.actionVariables.state.monitorId', + { + defaultMessage: 'ID of the monitor.', + } + ), + }, + { + name: 'monitorUrl', + description: i18n.translate( + 'xpack.uptime.alerts.durationAnomaly.actionVariables.state.monitorUrl', + { + defaultMessage: 'URL of the monitor.', + } + ), + }, + { + name: 'slowestAnomalyResponse', + description: i18n.translate( + 'xpack.uptime.alerts.durationAnomaly.actionVariables.state.slowestAnomalyResponse', + { + defaultMessage: 'Slowest response time during anomaly bucket with unit (ms, s) attached.', + } + ), + }, + { + name: 'expectedResponseTime', + description: i18n.translate( + 'xpack.uptime.alerts.durationAnomaly.actionVariables.state.expectedResponseTime', + { + defaultMessage: 'Expected response time', + } + ), + }, + { + name: 'severityScore', + description: i18n.translate( + 'xpack.uptime.alerts.durationAnomaly.actionVariables.state.severityScore', + { + defaultMessage: 'Anomaly severity score', + } + ), + }, + { + name: 'observerLocation', + description: i18n.translate( + 'xpack.uptime.alerts.durationAnomaly.actionVariables.state.observerLocation', + { + defaultMessage: 'Observer location from which heartbeat check is performed.', + } + ), + }, + ], +}; diff --git a/x-pack/plugins/uptime/server/lib/alerts/types.ts b/x-pack/plugins/uptime/server/lib/alerts/types.ts index a321cc124ac22..172930bc3dd3b 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/types.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/types.ts @@ -5,7 +5,11 @@ */ import { AlertType } from '../../../../alerts/server'; -import { UptimeCoreSetup } from '../adapters'; +import { UptimeCorePlugins, UptimeCoreSetup } from '../adapters'; import { UMServerLibs } from '../lib'; -export type UptimeAlertTypeFactory = (server: UptimeCoreSetup, libs: UMServerLibs) => AlertType; +export type UptimeAlertTypeFactory = ( + server: UptimeCoreSetup, + libs: UMServerLibs, + plugins: UptimeCorePlugins +) => AlertType; diff --git a/x-pack/plugins/uptime/server/uptime_server.ts b/x-pack/plugins/uptime/server/uptime_server.ts index fb90dfe2be6c5..afad5896ae64b 100644 --- a/x-pack/plugins/uptime/server/uptime_server.ts +++ b/x-pack/plugins/uptime/server/uptime_server.ts @@ -19,6 +19,6 @@ export const initUptimeServer = ( ); uptimeAlertTypeFactories.forEach((alertTypeFactory) => - plugins.alerts.registerType(alertTypeFactory(server, libs)) + plugins.alerts.registerType(alertTypeFactory(server, libs, plugins)) ); }; diff --git a/x-pack/test/functional/services/uptime/ml_anomaly.ts b/x-pack/test/functional/services/uptime/ml_anomaly.ts index a5f138b7a5716..ac9f6ab2b3d14 100644 --- a/x-pack/test/functional/services/uptime/ml_anomaly.ts +++ b/x-pack/test/functional/services/uptime/ml_anomaly.ts @@ -20,12 +20,18 @@ export function UptimeMLAnomalyProvider({ getService }: FtrProviderContext) { }, async openMLManageMenu() { + await this.cancelAlertFlyout(); return retry.tryForTime(30000, async () => { await testSubjects.click('uptimeManageMLJobBtn'); await testSubjects.existOrFail('uptimeManageMLContextMenu'); }); }, + async cancelAlertFlyout() { + if (await testSubjects.exists('euiFlyoutCloseButton')) + await testSubjects.click('euiFlyoutCloseButton', 60 * 1000); + }, + async alreadyHasJob() { return await testSubjects.exists('uptimeManageMLJobBtn'); }, @@ -55,5 +61,19 @@ export function UptimeMLAnomalyProvider({ getService }: FtrProviderContext) { async hasNoLicenseInfo() { return await testSubjects.missingOrFail('uptimeMLLicenseInfo', { timeout: 1000 }); }, + + async openAlertFlyout() { + return await testSubjects.click('uptimeEnableAnomalyAlertBtn'); + }, + + async disableAnomalyAlertIsVisible() { + return await testSubjects.exists('uptimeDisableAnomalyAlertBtn'); + }, + + async changeAlertThreshold(level: string) { + await testSubjects.click('uptimeAnomalySeverity'); + await testSubjects.click('anomalySeveritySelect'); + await testSubjects.click(`alertAnomaly${level}`); + }, }; } diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/anomaly_alert.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/anomaly_alert.ts new file mode 100644 index 0000000000000..03343bff642c3 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/anomaly_alert.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + describe('uptime anomaly alert', () => { + const pageObjects = getPageObjects(['common', 'uptime']); + const supertest = getService('supertest'); + const retry = getService('retry'); + + const monitorId = '0000-intermittent'; + + const uptime = getService('uptime'); + + const DEFAULT_DATE_START = 'Sep 10, 2019 @ 12:40:08.078'; + const DEFAULT_DATE_END = 'Sep 11, 2019 @ 19:40:08.078'; + let alerts: any; + const alertId = 'uptime-anomaly-alert'; + + before(async () => { + alerts = getService('uptime').alerts; + + await uptime.navigation.goToUptime(); + + await uptime.navigation.loadDataAndGoToMonitorPage( + DEFAULT_DATE_START, + DEFAULT_DATE_END, + monitorId + ); + }); + + it('can delete existing job', async () => { + if (await uptime.ml.alreadyHasJob()) { + await uptime.ml.openMLManageMenu(); + await uptime.ml.deleteMLJob(); + await uptime.navigation.refreshApp(); + } + }); + + it('can open ml flyout', async () => { + await uptime.ml.openMLFlyout(); + }); + + it('has permission to create job', async () => { + expect(uptime.ml.canCreateJob()).to.eql(true); + expect(uptime.ml.hasNoLicenseInfo()).to.eql(false); + }); + + it('can create job successfully', async () => { + await uptime.ml.createMLJob(); + await pageObjects.common.closeToast(); + await uptime.ml.cancelAlertFlyout(); + }); + + it('can open ML Manage Menu', async () => { + await uptime.ml.openMLManageMenu(); + }); + + it('can open anomaly alert flyout', async () => { + await uptime.ml.openAlertFlyout(); + }); + + it('can set alert name', async () => { + await alerts.setAlertName(alertId); + }); + + it('can set alert tags', async () => { + await alerts.setAlertTags(['uptime', 'anomaly-alert']); + }); + + it('can change anomaly alert threshold', async () => { + await uptime.ml.changeAlertThreshold('major'); + }); + + it('can save alert', async () => { + await alerts.clickSaveAlertButton(); + await pageObjects.common.closeToast(); + }); + + it('has created a valid alert with expected parameters', async () => { + let alert: any; + await retry.tryForTime(15000, async () => { + const apiResponse = await supertest.get(`/api/alerts/_find?search=${alertId}`); + const alertsFromThisTest = apiResponse.body.data.filter( + ({ name }: { name: string }) => name === alertId + ); + expect(alertsFromThisTest).to.have.length(1); + alert = alertsFromThisTest[0]; + }); + + // Ensure the parameters and other stateful data + // on the alert match up with the values we provided + // for our test helper to input into the flyout. + const { actions, alertTypeId, consumer, id, params, tags } = alert; + try { + expect(actions).to.eql([]); + expect(alertTypeId).to.eql('xpack.uptime.alerts.durationAnomaly'); + expect(consumer).to.eql('uptime'); + expect(tags).to.eql(['uptime', 'anomaly-alert']); + expect(params.monitorId).to.eql(monitorId); + expect(params.severity).to.eql(50); + } finally { + await supertest.delete(`/api/alerts/alert/${id}`).set('kbn-xsrf', 'true').expect(204); + } + }); + + it('change button to disable anomaly alert', async () => { + await uptime.ml.openMLManageMenu(); + expect(uptime.ml.disableAnomalyAlertIsVisible()).to.eql(true); + }); + + it('can delete job successfully', async () => { + await uptime.ml.deleteMLJob(); + }); + + it('verifies that alert is also deleted', async () => { + await retry.tryForTime(15000, async () => { + const apiResponse = await supertest.get(`/api/alerts/_find?search=${alertId}`); + const alertsFromThisTest = apiResponse.body.data.filter( + ({ name }: { name: string }) => name === alertId + ); + expect(alertsFromThisTest).to.have.length(0); + }); + }); + }); +}; diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/index.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/index.ts index ce91a2a26ce91..3016bd6d68f95 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/index.ts @@ -22,6 +22,7 @@ export default ({ getService, loadTestFile }: FtrProviderContext) => { after(async () => await esArchiver.unload(ARCHIVE)); loadTestFile(require.resolve('./alert_flyout')); + loadTestFile(require.resolve('./anomaly_alert')); }); }); }; From f0e75e80b5b33a2e9d09ed802a6284e1c2800e42 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 14 Jul 2020 19:56:49 +0200 Subject: [PATCH 22/82] updates edit exception text save button (#71684) --- .../exceptions/edit_exception_modal/index.tsx | 4 ++-- .../exceptions/edit_exception_modal/translations.ts | 11 +++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index cedf5c53e0ddc..73933d483e2cb 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -198,7 +198,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({ - {i18n.EDIT_EXCEPTION} + {i18n.EDIT_EXCEPTION_TITLE} {ruleName} @@ -260,7 +260,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({ {i18n.CANCEL} - {i18n.EDIT_EXCEPTION} + {i18n.EDIT_EXCEPTION_SAVE_BUTTON} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/translations.ts index b2d01d72131b4..6c5cb733b7a73 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/translations.ts @@ -10,8 +10,15 @@ export const CANCEL = i18n.translate('xpack.securitySolution.exceptions.editExce defaultMessage: 'Cancel', }); -export const EDIT_EXCEPTION = i18n.translate( - 'xpack.securitySolution.exceptions.editException.editException', +export const EDIT_EXCEPTION_SAVE_BUTTON = i18n.translate( + 'xpack.securitySolution.exceptions.editException.editExceptionSaveButton', + { + defaultMessage: 'Save', + } +); + +export const EDIT_EXCEPTION_TITLE = i18n.translate( + 'xpack.securitySolution.exceptions.editException.editExceptionTitle', { defaultMessage: 'Edit Exception', } From d0c9fe92840357b19eaea86d876b5c78b3ec0511 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 14 Jul 2020 19:08:19 +0100 Subject: [PATCH 23/82] merged lodash imports (#71672) This is just a code cleanup. A previous PR accidentally added a second import of the same module into alerts_client.ts. This PR corrects that. --- x-pack/plugins/alerts/server/alerts_client.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index ba832c65319f9..e49745b186bb3 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { omit, isEqual, map } from 'lodash'; +import { omit, isEqual, map, truncate } from 'lodash'; import { i18n } from '@kbn/i18n'; import { Logger, @@ -13,7 +13,6 @@ import { SavedObjectReference, SavedObject, } from 'src/core/server'; -import _ from 'lodash'; import { ActionsClient } from '../../actions/server'; import { Alert, @@ -713,6 +712,6 @@ export class AlertsClient { } private generateAPIKeyName(alertTypeId: string, alertName: string) { - return _.truncate(`Alerting: ${alertTypeId}/${alertName}`, { length: 256 }); + return truncate(`Alerting: ${alertTypeId}/${alertName}`, { length: 256 }); } } From 23ddd27f941cf0ddbf2494cae8dc77d9892f6e26 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Tue, 14 Jul 2020 14:32:45 -0400 Subject: [PATCH 24/82] [EPM][IngestManager][SecuritySolution] Correctly handle nested types (#71680) * Correctly handling nested types * Correct test names --- .../server/services/epm/fields/field.test.ts | 175 ++++++++++++++++++ .../server/services/epm/fields/field.ts | 19 +- 2 files changed, 190 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts index f0ff4c6125452..abd2ba777e516 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts @@ -269,6 +269,181 @@ describe('processFields', () => { expect(processFields(nested)).toEqual(nestedExpanded); }); + test('correctly handles properties of nested and object type fields together', () => { + const fields = [ + { + name: 'a', + type: 'object', + }, + { + name: 'a.b', + type: 'nested', + }, + { + name: 'a.b.c', + type: 'boolean', + }, + { + name: 'a.b.d', + type: 'keyword', + }, + ]; + + const fieldsExpanded = [ + { + name: 'a', + type: 'group', + fields: [ + { + name: 'b', + type: 'group-nested', + fields: [ + { + name: 'c', + type: 'boolean', + }, + { + name: 'd', + type: 'keyword', + }, + ], + }, + ], + }, + ]; + expect(processFields(fields)).toEqual(fieldsExpanded); + }); + + test('correctly handles properties of nested and object type fields in large depth', () => { + const fields = [ + { + name: 'a.h-object', + type: 'object', + dynamic: false, + }, + { + name: 'a.b-nested.c-nested', + type: 'nested', + }, + { + name: 'a.b-nested', + type: 'nested', + }, + { + name: 'a', + type: 'object', + }, + { + name: 'a.b-nested.d', + type: 'keyword', + }, + { + name: 'a.b-nested.c-nested.e', + type: 'boolean', + dynamic: true, + }, + { + name: 'a.b-nested.c-nested.f-object', + type: 'object', + }, + { + name: 'a.b-nested.c-nested.f-object.g', + type: 'keyword', + }, + ]; + + const fieldsExpanded = [ + { + name: 'a', + type: 'group', + fields: [ + { + name: 'h-object', + type: 'object', + dynamic: false, + }, + { + name: 'b-nested', + type: 'group-nested', + fields: [ + { + name: 'c-nested', + type: 'group-nested', + fields: [ + { + name: 'e', + type: 'boolean', + dynamic: true, + }, + { + name: 'f-object', + type: 'group', + fields: [ + { + name: 'g', + type: 'keyword', + }, + ], + }, + ], + }, + { + name: 'd', + type: 'keyword', + }, + ], + }, + ], + }, + ]; + expect(processFields(fields)).toEqual(fieldsExpanded); + }); + + test('correctly handles properties of nested and object type fields together in different order', () => { + const fields = [ + { + name: 'a.b.c', + type: 'boolean', + }, + { + name: 'a.b', + type: 'nested', + }, + { + name: 'a', + type: 'object', + }, + { + name: 'a.b.d', + type: 'keyword', + }, + ]; + + const fieldsExpanded = [ + { + name: 'a', + type: 'group', + fields: [ + { + name: 'b', + type: 'group-nested', + fields: [ + { + name: 'c', + type: 'boolean', + }, + { + name: 'd', + type: 'keyword', + }, + ], + }, + ], + }, + ]; + expect(processFields(fields)).toEqual(fieldsExpanded); + }); + test('correctly handles properties of nested type where nested top level comes second', () => { const nested = [ { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts index e7c0eca2a9613..a44e5e4221f9f 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts @@ -126,10 +126,21 @@ function dedupFields(fields: Fields): Fields { if ( // only merge if found is a group and field is object, nested, or group. // Or if found is object, or nested, and field is a group. - // This is to avoid merging two objects, or nested, or object with a nested. + // This is to avoid merging two objects, or two nested, or object with a nested. + + // we do not need to check for group-nested in this part because `field` will never have group-nested + // it can only exist on `found` (found.type === 'group' && (field.type === 'object' || field.type === 'nested' || field.type === 'group')) || - ((found.type === 'object' || found.type === 'nested') && field.type === 'group') + // as part of the loop we will be marking found.type as group-nested so found could be group-nested if it was + // already processed. If we had an explicit definition of nested, and it showed up before a descendant field: + // - name: a + // type: nested + // - name: a.b + // type: keyword + // then found.type will be nested and not group-nested because it won't have any fields yet until a.b is processed + ((found.type === 'object' || found.type === 'nested' || found.type === 'group-nested') && + field.type === 'group') ) { // if the new field has properties let's dedup and concat them with the already existing found variable in // the array @@ -148,10 +159,10 @@ function dedupFields(fields: Fields): Fields { // supposed to be `nested` for when the template is actually generated if (found.type === 'nested' || field.type === 'nested') { found.type = 'group-nested'; - } else { - // found was either `group` already or `object` so just set it to `group` + } else if (found.type === 'object') { found.type = 'group'; } + // found.type could be group-nested or group, in those cases just leave it } // we need to merge in other properties (like `dynamic`) that might exist Object.assign(found, importantFieldProps); From 8db71dee09a1a99cb95123a592e68ba57ddf28fa Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Tue, 14 Jul 2020 12:43:08 -0600 Subject: [PATCH 25/82] [DOCS] Clarify 'fields' option in SO.find docs (#71491) --- docs/api/saved-objects/bulk_get.asciidoc | 2 +- docs/api/saved-objects/find.asciidoc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api/saved-objects/bulk_get.asciidoc b/docs/api/saved-objects/bulk_get.asciidoc index eaf91a662849e..1d2c9cc32d431 100644 --- a/docs/api/saved-objects/bulk_get.asciidoc +++ b/docs/api/saved-objects/bulk_get.asciidoc @@ -29,7 +29,7 @@ experimental[] Retrieve multiple {kib} saved objects by ID. (Required, string) ID of the retrieved object. The ID includes the {kib} unique identifier or a custom identifier. `fields`:: - (Optional, array) The fields returned in the object response. + (Optional, array) The fields to return in the `attributes` key of the object response. [[saved-objects-api-bulk-get-response-body]] ==== Response body diff --git a/docs/api/saved-objects/find.asciidoc b/docs/api/saved-objects/find.asciidoc index 93e60be5d4923..e82c4e0c00d11 100644 --- a/docs/api/saved-objects/find.asciidoc +++ b/docs/api/saved-objects/find.asciidoc @@ -41,7 +41,7 @@ experimental[] Retrieve a paginated set of {kib} saved objects by various condit (Optional, array|string) The fields to perform the `simple_query_string` parsed query against. `fields`:: - (Optional, array|string) The fields to return in the response. + (Optional, array|string) The fields to return in the `attributes` key of the response. `sort_field`:: (Optional, string) The field that sorts the response. From 6e30ce1ff2fd0456da6e507674b58e0430ed2266 Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Tue, 14 Jul 2020 19:45:10 +0100 Subject: [PATCH 26/82] [ML] Fix error toasts shown when starting or editing jobs (#71618) * [ML] Fix error toasts shown when starting or editing jobs * [ML] Adds toast_notification_service.ts file * [ML] Fix Jest and type_check tests * [ML] Alter check for statusCode in error object handling * [ML] Fix errors Jest test --- x-pack/plugins/ml/common/util/errors.test.ts | 2 + x-pack/plugins/ml/common/util/errors.ts | 102 +++++++++++++++--- .../action_delete/action_delete.test.tsx | 6 ++ .../action_delete/use_delete_action.ts | 8 +- .../action_edit/edit_button_flyout.tsx | 14 +-- .../action_start/use_start_action.ts | 5 +- .../analytics_service/delete_analytics.ts | 38 +++---- .../analytics_service/start_analytics.ts | 19 ++-- .../edit_job_flyout/edit_job_flyout.js | 8 +- .../jobs/jobs_list/components/utils.js | 6 +- .../application/services/job_service.js | 26 +++-- .../services/toast_notification_service.ts | 84 +++++++++++++++ .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - 14 files changed, 256 insertions(+), 70 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/services/toast_notification_service.ts diff --git a/x-pack/plugins/ml/common/util/errors.test.ts b/x-pack/plugins/ml/common/util/errors.test.ts index 00af27248ccce..0b99799e3b6ec 100644 --- a/x-pack/plugins/ml/common/util/errors.test.ts +++ b/x-pack/plugins/ml/common/util/errors.test.ts @@ -30,6 +30,8 @@ describe('ML - error message utils', () => { const bodyWithStringMsg: MLCustomHttpResponseOptions = { body: { msg: testMsg, + statusCode: 404, + response: `{"error":{"reason":"${testMsg}"}}`, }, statusCode: 404, }; diff --git a/x-pack/plugins/ml/common/util/errors.ts b/x-pack/plugins/ml/common/util/errors.ts index e165e15d7c64e..6c5fa7bd75daf 100644 --- a/x-pack/plugins/ml/common/util/errors.ts +++ b/x-pack/plugins/ml/common/util/errors.ts @@ -41,7 +41,7 @@ export type MLResponseError = msg: string; }; } - | { msg: string }; + | { msg: string; statusCode: number; response: string }; export interface MLCustomHttpResponseOptions< T extends ResponseError | MLResponseError | BoomResponse @@ -53,42 +53,118 @@ export interface MLCustomHttpResponseOptions< statusCode: number; } -export const extractErrorMessage = ( +export interface MLErrorObject { + message: string; + fullErrorMessage?: string; // For use in a 'See full error' popover. + statusCode?: number; +} + +export const extractErrorProperties = ( error: | MLCustomHttpResponseOptions - | undefined | string -): string => { - // extract only the error message within the response error coming from Kibana, Elasticsearch, and our own ML messages + | undefined +): MLErrorObject => { + // extract properties of the error object from within the response error + // coming from Kibana, Elasticsearch, and our own ML messages + let message = ''; + let fullErrorMessage; + let statusCode; if (typeof error === 'string') { - return error; + return { + message: error, + }; + } + if (error?.body === undefined) { + return { + message: '', + }; } - if (error?.body === undefined) return ''; if (typeof error.body === 'string') { - return error.body; + return { + message: error.body, + }; } if ( typeof error.body === 'object' && 'output' in error.body && error.body.output.payload.message ) { - return error.body.output.payload.message; + return { + message: error.body.output.payload.message, + }; + } + + if ( + typeof error.body === 'object' && + 'response' in error.body && + typeof error.body.response === 'string' + ) { + const errorResponse = JSON.parse(error.body.response); + if ('error' in errorResponse && typeof errorResponse === 'object') { + const errorResponseError = errorResponse.error; + if ('reason' in errorResponseError) { + message = errorResponseError.reason; + } + if ('caused_by' in errorResponseError) { + const causedByMessage = JSON.stringify(errorResponseError.caused_by); + // Only add a fullErrorMessage if different to the message. + if (causedByMessage !== message) { + fullErrorMessage = causedByMessage; + } + } + return { + message, + fullErrorMessage, + statusCode: error.statusCode, + }; + } } if (typeof error.body === 'object' && 'msg' in error.body && typeof error.body.msg === 'string') { - return error.body.msg; + return { + message: error.body.msg, + }; } if (typeof error.body === 'object' && 'message' in error.body) { + if ( + 'attributes' in error.body && + typeof error.body.attributes === 'object' && + error.body.attributes.body?.status !== undefined + ) { + statusCode = error.body.attributes.body?.status; + } + if (typeof error.body.message === 'string') { - return error.body.message; + return { + message: error.body.message, + statusCode, + }; } if (!(error.body.message instanceof Error) && typeof (error.body.message.msg === 'string')) { - return error.body.message.msg; + return { + message: error.body.message.msg, + statusCode, + }; } } + // If all else fail return an empty message instead of JSON.stringify - return ''; + return { + message: '', + }; +}; + +export const extractErrorMessage = ( + error: + | MLCustomHttpResponseOptions + | undefined + | string +): 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/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/action_delete.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/action_delete.test.tsx index 8d6272c5df860..6b745a2c5ff3b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/action_delete.test.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/action_delete.test.tsx @@ -31,7 +31,13 @@ jest.mock('../../../../../contexts/kibana', () => ({ useMlKibana: () => ({ services: mockCoreServices.createStart(), }), + useNotifications: () => { + return { + toasts: { addSuccess: jest.fn(), addDanger: jest.fn(), addError: jest.fn() }, + }; + }, })); + export const MockI18nService = i18nServiceMock.create(); export const I18nServiceConstructor = jest.fn().mockImplementation(() => MockI18nService); jest.doMock('@kbn/i18n', () => ({ diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.ts index f924cf3afcba5..4fc7b5e1367c4 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.ts @@ -13,6 +13,7 @@ import { IIndexPattern } from 'src/plugins/data/common'; import { extractErrorMessage } from '../../../../../../../common/util/errors'; import { useMlKibana } from '../../../../../contexts/kibana'; +import { useToastNotificationService } from '../../../../../services/toast_notification_service'; import { deleteAnalytics, @@ -37,6 +38,8 @@ export const useDeleteAction = () => { const indexName = item?.config.dest.index ?? ''; + const toastNotificationService = useToastNotificationService(); + const checkIndexPatternExists = async () => { try { const response = await savedObjectsClient.find({ @@ -109,10 +112,11 @@ export const useDeleteAction = () => { deleteAnalyticsAndDestIndex( item, deleteTargetIndex, - indexPatternExists && deleteIndexPattern + indexPatternExists && deleteIndexPattern, + toastNotificationService ); } else { - deleteAnalytics(item); + deleteAnalytics(item, toastNotificationService); } } }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button_flyout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button_flyout.tsx index 4b708d48ca0ec..86b1c879417bb 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button_flyout.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button_flyout.tsx @@ -28,11 +28,11 @@ import { import { useMlKibana } from '../../../../../contexts/kibana'; import { ml } from '../../../../../services/ml_api_service'; +import { useToastNotificationService } from '../../../../../services/toast_notification_service'; import { memoryInputValidator, MemoryInputValidatorResult, } from '../../../../../../../common/util/validators'; -import { extractErrorMessage } from '../../../../../../../common/util/errors'; import { DATA_FRAME_TASK_STATE } from '../analytics_list/common'; import { useRefreshAnalyticsList, @@ -60,6 +60,8 @@ export const EditButtonFlyout: FC> = ({ closeFlyout, item } } = useMlKibana(); const { refresh } = useRefreshAnalyticsList(); + const toastNotificationService = useToastNotificationService(); + // Disable if mml is not valid const updateButtonDisabled = mmlValidationError !== undefined || maxNumThreads === 0; @@ -113,15 +115,15 @@ export const EditButtonFlyout: FC> = ({ closeFlyout, item } // eslint-disable-next-line console.error(e); - notifications.toasts.addDanger({ - title: i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutErrorMessage', { + toastNotificationService.displayErrorToast( + e, + i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutErrorMessage', { defaultMessage: 'Could not save changes to analytics job {jobId}', values: { jobId, }, - }), - text: extractErrorMessage(e), - }); + }) + ); } }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/use_start_action.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/use_start_action.ts index 8eb6b990827ac..3c1087ff587d8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/use_start_action.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/use_start_action.ts @@ -8,6 +8,7 @@ import { useState } from 'react'; import { DataFrameAnalyticsListRow } from '../analytics_list/common'; import { startAnalytics } from '../../services/analytics_service'; +import { useToastNotificationService } from '../../../../../services/toast_notification_service'; export type StartAction = ReturnType; export const useStartAction = () => { @@ -15,11 +16,13 @@ export const useStartAction = () => { const [item, setItem] = useState(); + const toastNotificationService = useToastNotificationService(); + const closeModal = () => setModalVisible(false); const startAndCloseModal = () => { if (item !== undefined) { setModalVisible(false); - startAnalytics(item); + startAnalytics(item, toastNotificationService); } }; 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 ebd3fa8982604..7d3ee986a4ef1 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 @@ -7,13 +7,17 @@ import { i18n } from '@kbn/i18n'; import { extractErrorMessage } from '../../../../../../../common/util/errors'; import { getToastNotifications } from '../../../../../util/dependency_cache'; import { ml } from '../../../../../services/ml_api_service'; +import { ToastNotificationService } from '../../../../../services/toast_notification_service'; import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../../common'; import { isDataFrameAnalyticsFailed, DataFrameAnalyticsListRow, } from '../../components/analytics_list/common'; -export const deleteAnalytics = async (d: DataFrameAnalyticsListRow) => { +export const deleteAnalytics = async ( + d: DataFrameAnalyticsListRow, + toastNotificationService: ToastNotificationService +) => { const toastNotifications = getToastNotifications(); try { if (isDataFrameAnalyticsFailed(d.stats.state)) { @@ -27,13 +31,11 @@ export const deleteAnalytics = async (d: DataFrameAnalyticsListRow) => { }) ); } catch (e) { - const error = extractErrorMessage(e); - - toastNotifications.addDanger( + toastNotificationService.displayErrorToast( + e, i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage', { - defaultMessage: - 'An error occurred deleting the data frame analytics job {analyticsId}: {error}', - values: { analyticsId: d.config.id, error }, + defaultMessage: 'An error occurred deleting the data frame analytics job {analyticsId}', + values: { analyticsId: d.config.id }, }) ); } @@ -43,7 +45,8 @@ export const deleteAnalytics = async (d: DataFrameAnalyticsListRow) => { export const deleteAnalyticsAndDestIndex = async ( d: DataFrameAnalyticsListRow, deleteDestIndex: boolean, - deleteDestIndexPattern: boolean + deleteDestIndexPattern: boolean, + toastNotificationService: ToastNotificationService ) => { const toastNotifications = getToastNotifications(); const destinationIndex = Array.isArray(d.config.dest.index) @@ -67,12 +70,11 @@ export const deleteAnalyticsAndDestIndex = async ( ); } if (status.analyticsJobDeleted?.error) { - const error = extractErrorMessage(status.analyticsJobDeleted.error); - toastNotifications.addDanger( + toastNotificationService.displayErrorToast( + status.analyticsJobDeleted.error, i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage', { - defaultMessage: - 'An error occurred deleting the data frame analytics job {analyticsId}: {error}', - values: { analyticsId: d.config.id, error }, + defaultMessage: 'An error occurred deleting the data frame analytics job {analyticsId}', + values: { analyticsId: d.config.id }, }) ); } @@ -120,13 +122,11 @@ export const deleteAnalyticsAndDestIndex = async ( ); } } catch (e) { - const error = extractErrorMessage(e); - - toastNotifications.addDanger( + toastNotificationService.displayErrorToast( + e, i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage', { - defaultMessage: - 'An error occurred deleting the data frame analytics job {analyticsId}: {error}', - values: { analyticsId: d.config.id, error }, + defaultMessage: 'An error occurred deleting the data frame analytics job {analyticsId}', + values: { analyticsId: d.config.id }, }) ); } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/start_analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/start_analytics.ts index 6513cad808485..dfaac8f391f3c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/start_analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/start_analytics.ts @@ -5,29 +5,30 @@ */ import { i18n } from '@kbn/i18n'; -import { getToastNotifications } from '../../../../../util/dependency_cache'; import { ml } from '../../../../../services/ml_api_service'; +import { ToastNotificationService } from '../../../../../services/toast_notification_service'; import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../../common'; import { DataFrameAnalyticsListRow } from '../../components/analytics_list/common'; -export const startAnalytics = async (d: DataFrameAnalyticsListRow) => { - const toastNotifications = getToastNotifications(); +export const startAnalytics = async ( + d: DataFrameAnalyticsListRow, + toastNotificationService: ToastNotificationService +) => { try { await ml.dataFrameAnalytics.startDataFrameAnalytics(d.config.id); - toastNotifications.addSuccess( + toastNotificationService.displaySuccessToast( i18n.translate('xpack.ml.dataframe.analyticsList.startAnalyticsSuccessMessage', { defaultMessage: 'Request to start data frame analytics {analyticsId} acknowledged.', values: { analyticsId: d.config.id }, }) ); } catch (e) { - toastNotifications.addDanger( - i18n.translate('xpack.ml.dataframe.analyticsList.startAnalyticsErrorMessage', { - defaultMessage: - 'An error occurred starting the data frame analytics {analyticsId}: {error}', - values: { analyticsId: d.config.id, error: JSON.stringify(e) }, + toastNotificationService.displayErrorToast( + e, + i18n.translate('xpack.ml.dataframe.analyticsList.startAnalyticsErrorTitle', { + defaultMessage: 'Error starting job', }) ); } diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js index 3508d69ee2212..9d0082ffcb568 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js @@ -26,7 +26,7 @@ import { JobDetails, Detectors, Datafeed, CustomUrls } from './tabs'; import { saveJob } from './edit_utils'; import { loadFullJob } from '../utils'; import { validateModelMemoryLimit, validateGroupNames, isValidCustomUrls } from '../validate_job'; -import { mlMessageBarService } from '../../../../components/messagebar'; +import { toastNotificationServiceProvider } from '../../../../services/toast_notification_service'; import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -255,6 +255,8 @@ export class EditJobFlyoutUI extends Component { }; const { toasts } = this.props.kibana.services.notifications; + const toastNotificationService = toastNotificationServiceProvider(toasts); + saveJob(this.state.job, newJobData) .then(() => { toasts.addSuccess( @@ -270,7 +272,8 @@ export class EditJobFlyoutUI extends Component { }) .catch((error) => { console.error(error); - toasts.addDanger( + toastNotificationService.displayErrorToast( + error, i18n.translate('xpack.ml.jobsList.editJobFlyout.changesNotSavedNotificationMessage', { defaultMessage: 'Could not save changes to {jobId}', values: { @@ -278,7 +281,6 @@ export class EditJobFlyoutUI extends Component { }, }) ); - mlMessageBarService.notify.error(error); }); }; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js index 569eca4aba949..6fabd0299a936 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js @@ -9,6 +9,7 @@ import { mlMessageBarService } from '../../../components/messagebar'; import rison from 'rison-node'; import { mlJobService } from '../../../services/job_service'; +import { toastNotificationServiceProvider } from '../../../services/toast_notification_service'; import { ml } from '../../../services/ml_api_service'; import { getToastNotifications } from '../../../util/dependency_cache'; import { stringMatch } from '../../../util/string_utils'; @@ -158,8 +159,9 @@ function showResults(resp, action) { if (failures.length > 0) { failures.forEach((f) => { - mlMessageBarService.notify.error(f.result.error); - toastNotifications.addDanger( + const toastNotificationService = toastNotificationServiceProvider(toastNotifications); + toastNotificationService.displayErrorToast( + f.result.error, i18n.translate('xpack.ml.jobsList.actionFailedNotificationMessage', { defaultMessage: '{failureId} failed to {actionText}', values: { diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js index 6c0f393c267aa..7e90758ffd7db 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -11,10 +11,12 @@ import { i18n } from '@kbn/i18n'; import { ml } from './ml_api_service'; import { mlMessageBarService } from '../components/messagebar'; +import { getToastNotifications } from '../util/dependency_cache'; import { isWebUrl } from '../util/url_utils'; import { ML_DATA_PREVIEW_COUNT } from '../../../common/util/job_utils'; import { TIME_FORMAT } from '../../../common/constants/time_format'; import { parseInterval } from '../../../common/util/parse_interval'; +import { toastNotificationServiceProvider } from '../services/toast_notification_service'; const msgs = mlMessageBarService; let jobs = []; @@ -417,14 +419,21 @@ class JobService { return { success: true }; }) .catch((err) => { - msgs.notify.error( - i18n.translate('xpack.ml.jobService.couldNotUpdateJobErrorMessage', { + // TODO - all the functions in here should just return the error and not + // display the toast, as currently both the component and this service display + // errors, so we end up with duplicate toasts. + const toastNotifications = getToastNotifications(); + const toastNotificationService = toastNotificationServiceProvider(toastNotifications); + toastNotificationService.displayErrorToast( + err, + i18n.translate('xpack.ml.jobService.updateJobErrorTitle', { defaultMessage: 'Could not update job: {jobId}', values: { jobId }, }) ); + console.error('update job', err); - return { success: false, message: err.message }; + return { success: false, message: err }; }); } @@ -436,12 +445,15 @@ class JobService { return { success: true, messages }; }) .catch((err) => { - msgs.notify.error( - i18n.translate('xpack.ml.jobService.jobValidationErrorMessage', { - defaultMessage: 'Job Validation Error: {errorMessage}', - values: { errorMessage: err.message }, + const toastNotifications = getToastNotifications(); + const toastNotificationService = toastNotificationServiceProvider(toastNotifications); + toastNotificationService.displayErrorToast( + err, + i18n.translate('xpack.ml.jobService.validateJobErrorTitle', { + defaultMessage: 'Job Validation Error', }) ); + console.log('validate job', err); return { success: false, diff --git a/x-pack/plugins/ml/public/application/services/toast_notification_service.ts b/x-pack/plugins/ml/public/application/services/toast_notification_service.ts new file mode 100644 index 0000000000000..d93d6833c7cb4 --- /dev/null +++ b/x-pack/plugins/ml/public/application/services/toast_notification_service.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ToastInput, ToastOptions, ToastsStart } from 'kibana/public'; +import { ResponseError } from 'kibana/server'; +import { useMemo } from 'react'; +import { useNotifications } from '../contexts/kibana'; +import { + BoomResponse, + extractErrorProperties, + MLCustomHttpResponseOptions, + MLErrorObject, + MLResponseError, +} from '../../../common/util/errors'; + +export type ToastNotificationService = ReturnType; + +export function toastNotificationServiceProvider(toastNotifications: ToastsStart) { + return { + displaySuccessToast(toastOrTitle: ToastInput, options?: ToastOptions) { + toastNotifications.addSuccess(toastOrTitle, options); + }, + + displayErrorToast(error: any, toastTitle: string) { + const errorObj = this.parseErrorMessage(error); + if (errorObj.fullErrorMessage !== undefined) { + // Provide access to the full error message via the 'See full error' button. + toastNotifications.addError(new Error(errorObj.fullErrorMessage), { + title: toastTitle, + toastMessage: errorObj.message, + }); + } else { + toastNotifications.addDanger( + { + title: toastTitle, + text: errorObj.message, + }, + { toastLifeTimeMs: 30000 } + ); + } + }, + + parseErrorMessage( + error: + | MLCustomHttpResponseOptions + | undefined + | string + | MLResponseError + ): MLErrorObject { + if ( + typeof error === 'object' && + 'response' in error && + typeof error.response === 'string' && + error.statusCode !== undefined + ) { + // MLResponseError which has been received back as part of a 'successful' response + // where the error was passed in a separate property in the response. + const wrapMlResponseError = { + body: error, + statusCode: error.statusCode, + }; + return extractErrorProperties(wrapMlResponseError); + } + + return extractErrorProperties( + error as + | MLCustomHttpResponseOptions + | undefined + | string + ); + }, + }; +} + +/** + * Hook to use {@link ToastNotificationService} in React components. + */ +export function useToastNotificationService(): ToastNotificationService { + const { toasts } = useNotifications(); + return useMemo(() => toastNotificationServiceProvider(toasts), []); +} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 287cf443b1b07..2a8365a8bc5c9 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9597,7 +9597,6 @@ "xpack.ml.dataframe.analyticsList.createDataFrameAnalyticsButton": "分析ジョブの作成", "xpack.ml.dataframe.analyticsList.deleteActionDisabledToolTipContent": "削除するにはデータフレーム分析を停止してください。", "xpack.ml.dataframe.analyticsList.deleteActionName": "削除", - "xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage": "データフレーム分析{analyticsId}の削除中にエラーが発生しました。{error}", "xpack.ml.dataframe.analyticsList.deleteAnalyticsSuccessMessage": "データフレーム分析 {analyticsId} の削除リクエストが受け付けられました。", "xpack.ml.dataframe.analyticsList.deleteModalBody": "この分析ジョブを削除してよろしいですか?この分析ジョブのデスティネーションインデックスとオプションのKibanaインデックスパターンは削除されません。", "xpack.ml.dataframe.analyticsList.deleteModalCancelButton": "キャンセル", @@ -9621,7 +9620,6 @@ "xpack.ml.dataframe.analyticsList.showDetailsColumn.screenReaderDescription": "このカラムには各ジョブの詳細を示すクリック可能なコントロールが含まれます", "xpack.ml.dataframe.analyticsList.sourceIndex": "ソースインデックス", "xpack.ml.dataframe.analyticsList.startActionName": "開始", - "xpack.ml.dataframe.analyticsList.startAnalyticsErrorMessage": "データフレーム分析{analyticsId}の開始中にエラーが発生しました。{error}", "xpack.ml.dataframe.analyticsList.startAnalyticsSuccessMessage": "データフレーム分析 {analyticsId} の開始リクエストが受け付けられました。", "xpack.ml.dataframe.analyticsList.startModalBody": "データフレーム分析ジョブは、クラスターの検索とインデックスによる負荷を増やします。過剰な負荷が生じた場合は分析ジョブを停止してください。この分析ジョブを開始してよろしいですか?", "xpack.ml.dataframe.analyticsList.startModalCancelButton": "キャンセル", @@ -9997,11 +9995,9 @@ "xpack.ml.jobService.couldNotStartDatafeedErrorMessage": "{jobId} のデータフィードを開始できませんでした", "xpack.ml.jobService.couldNotStopDatafeedErrorMessage": "{jobId} のデータフィードを停止できませんでした", "xpack.ml.jobService.couldNotUpdateDatafeedErrorMessage": "データフィードを更新できませんでした: {datafeedId}", - "xpack.ml.jobService.couldNotUpdateJobErrorMessage": "ジョブを更新できませんでした: {jobId}", "xpack.ml.jobService.datafeedsListCouldNotBeRetrievedErrorMessage": "データフィードリストを取得できませんでした", "xpack.ml.jobService.failedJobsLabel": "失敗したジョブ", "xpack.ml.jobService.jobsListCouldNotBeRetrievedErrorMessage": "ジョブリストを取得できませんでした", - "xpack.ml.jobService.jobValidationErrorMessage": "ジョブ検証エラー: {errorMessage}", "xpack.ml.jobService.openJobsLabel": "ジョブを開く", "xpack.ml.jobService.requestMayHaveTimedOutErrorMessage": "リクエストがタイムアウトし、まだバックグラウンドで実行中の可能性があります。", "xpack.ml.jobService.totalJobsLabel": "合計ジョブ数", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ea3aa71b154aa..42240203a2eaf 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9602,7 +9602,6 @@ "xpack.ml.dataframe.analyticsList.createDataFrameAnalyticsButton": "创建分析作业", "xpack.ml.dataframe.analyticsList.deleteActionDisabledToolTipContent": "停止数据帧分析,才能将其删除。", "xpack.ml.dataframe.analyticsList.deleteActionName": "删除", - "xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage": "删除数据帧分析 {analyticsId} 时发生错误:{error}", "xpack.ml.dataframe.analyticsList.deleteAnalyticsSuccessMessage": "数据帧分析 {analyticsId} 删除请求已确认。", "xpack.ml.dataframe.analyticsList.deleteModalBody": "是否确定要删除此分析作业?分析作业的目标索引和可选 Kibana 索引模式将不会删除。", "xpack.ml.dataframe.analyticsList.deleteModalCancelButton": "取消", @@ -9626,7 +9625,6 @@ "xpack.ml.dataframe.analyticsList.showDetailsColumn.screenReaderDescription": "此列包含可单击控件,用于显示每个作业的更多详情", "xpack.ml.dataframe.analyticsList.sourceIndex": "源索引", "xpack.ml.dataframe.analyticsList.startActionName": "开始", - "xpack.ml.dataframe.analyticsList.startAnalyticsErrorMessage": "启动数据帧分析 {analyticsId} 时发生错误:{error}", "xpack.ml.dataframe.analyticsList.startAnalyticsSuccessMessage": "数据帧分析 {analyticsId} 启动请求已确认。", "xpack.ml.dataframe.analyticsList.startModalBody": "数据帧分析作业将增加集群的搜索和索引负荷。如果负荷超载,请停止分析作业。是否确定要启动此分析作业?", "xpack.ml.dataframe.analyticsList.startModalCancelButton": "取消", @@ -10002,11 +10000,9 @@ "xpack.ml.jobService.couldNotStartDatafeedErrorMessage": "无法开始 {jobId} 的数据馈送", "xpack.ml.jobService.couldNotStopDatafeedErrorMessage": "无法停止 {jobId} 的数据馈送", "xpack.ml.jobService.couldNotUpdateDatafeedErrorMessage": "无法更新数据馈送:{datafeedId}", - "xpack.ml.jobService.couldNotUpdateJobErrorMessage": "无法更新作业:{jobId}", "xpack.ml.jobService.datafeedsListCouldNotBeRetrievedErrorMessage": "无法检索数据馈送列表", "xpack.ml.jobService.failedJobsLabel": "失败的作业", "xpack.ml.jobService.jobsListCouldNotBeRetrievedErrorMessage": "无法检索作业列表", - "xpack.ml.jobService.jobValidationErrorMessage": "作业验证错误:{errorMessage}", "xpack.ml.jobService.openJobsLabel": "打开的作业", "xpack.ml.jobService.requestMayHaveTimedOutErrorMessage": "请求可能已超时,并可能仍在后台运行。", "xpack.ml.jobService.totalJobsLabel": "总计作业数", From 513d0e09e1583370ad036b83d4503e08b4560098 Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 14 Jul 2020 11:49:04 -0700 Subject: [PATCH 27/82] skip flaky suite (#71713) --- src/plugins/vis_type_vega/public/vega_visualization.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/vis_type_vega/public/vega_visualization.test.js b/src/plugins/vis_type_vega/public/vega_visualization.test.js index a6ad6e4908bb4..108b34b36c66f 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.test.js +++ b/src/plugins/vis_type_vega/public/vega_visualization.test.js @@ -52,7 +52,8 @@ jest.mock('./lib/vega', () => ({ vegaLite: jest.requireActual('vega-lite'), })); -describe('VegaVisualizations', () => { +// FLAKY: https://github.com/elastic/kibana/issues/71713 +describe.skip('VegaVisualizations', () => { let domNode; let VegaVisualization; let vis; From 9e2ebe204070eb80ab8c035e8259bd41f9814291 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Tue, 14 Jul 2020 14:20:24 -0500 Subject: [PATCH 28/82] [Security Solution][Detections] Update telemetry to use ML contract (#71665) * Update security solution telemetry to use ML providers This interface recently changed and we're now able to use the ML contract to retrieve these values. A few unnecessary arguments are stubbed as we're in a non-user, non-request context. * Simplify our capabilities stub assignment This is more legible but still gets the point across; the intermediate variable was explicit but ultimately unnnecessary. * Update tests following telemetry refactor We're not calling different methods, so our mocks need to change slightly. --- .../shared_services/providers/modules.ts | 9 ++++- .../server/lib/machine_learning/mocks.ts | 2 + .../usage/detections/detections.test.ts | 15 +++----- .../usage/detections/detections_helpers.ts | 38 ++++++++----------- 4 files changed, 31 insertions(+), 33 deletions(-) diff --git a/x-pack/plugins/ml/server/shared_services/providers/modules.ts b/x-pack/plugins/ml/server/shared_services/providers/modules.ts index 33c8d28399a32..fb7d59f9c8218 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/modules.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/modules.ts @@ -13,6 +13,7 @@ import { TypeOf } from '@kbn/config-schema'; import { DataRecognizer } from '../../models/data_recognizer'; import { SharedServicesChecks } from '../shared_services'; import { moduleIdParamSchema, setupModuleBodySchema } from '../../routes/schemas/modules'; +import { HasMlCapabilities } from '../../lib/capabilities'; export type ModuleSetupPayload = TypeOf & TypeOf; @@ -40,8 +41,14 @@ export function getModulesProvider({ request: KibanaRequest, savedObjectsClient: SavedObjectsClientContract ) { - const hasMlCapabilities = getHasMlCapabilities(request); + let hasMlCapabilities: HasMlCapabilities; + if (request.params === 'DummyKibanaRequest') { + hasMlCapabilities = () => Promise.resolve(); + } else { + hasMlCapabilities = getHasMlCapabilities(request); + } const dr = dataRecognizerFactory(mlClusterClient, savedObjectsClient, request); + return { async recognize(...args) { isFullLicense(); diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/mocks.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/mocks.ts index e9b692e4731aa..73e9ae58244c1 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/mocks.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/mocks.ts @@ -16,6 +16,8 @@ const createMockMlSystemProvider = () => export const mlServicesMock = { create: () => (({ + modulesProvider: jest.fn(), + jobServiceProvider: jest.fn(), mlSystemProvider: createMockMlSystemProvider(), mlClient: createMockClient(), } as unknown) as jest.Mocked), diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts b/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts index 0fc23f90a0ebf..69ae53a14227d 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts @@ -6,8 +6,6 @@ import { LegacyAPICaller } from '../../../../../../src/core/server'; import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; -import { jobServiceProvider } from '../../../../ml/server/models/job_service'; -import { DataRecognizer } from '../../../../ml/server/models/data_recognizer'; import { mlServicesMock } from '../../lib/machine_learning/mocks'; import { getMockJobSummaryResponse, @@ -16,9 +14,6 @@ import { } from './detections.mocks'; import { fetchDetectionsUsage } from './index'; -jest.mock('../../../../ml/server/models/job_service'); -jest.mock('../../../../ml/server/models/data_recognizer'); - describe('Detections Usage', () => { describe('fetchDetectionsUsage()', () => { let callClusterMock: jest.Mocked; @@ -79,12 +74,12 @@ describe('Detections Usage', () => { it('tallies jobs data given jobs results', async () => { const mockJobSummary = jest.fn().mockResolvedValue(getMockJobSummaryResponse()); const mockListModules = jest.fn().mockResolvedValue(getMockListModulesResponse()); - (jobServiceProvider as jest.Mock).mockImplementation(() => ({ - jobsSummary: mockJobSummary, - })); - (DataRecognizer as jest.Mock).mockImplementation(() => ({ + mlMock.modulesProvider.mockReturnValue(({ listModules: mockListModules, - })); + } as unknown) as ReturnType); + mlMock.jobServiceProvider.mockReturnValue({ + jobsSummary: mockJobSummary, + }); const result = await fetchDetectionsUsage('', callClusterMock, mlMock); diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts index bad8ef235c6d6..e9d4f3aa426f4 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts @@ -5,13 +5,12 @@ */ import { SearchParams } from 'elasticsearch'; -import { ILegacyScopedClusterClient, KibanaRequest } from 'kibana/server'; -import { LegacyAPICaller, SavedObjectsClient } from '../../../../../../src/core/server'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { jobServiceProvider } from '../../../../ml/server/models/job_service'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { DataRecognizer } from '../../../../ml/server/models/data_recognizer'; +import { + LegacyAPICaller, + SavedObjectsClient, + KibanaRequest, +} from '../../../../../../src/core/server'; import { MlPluginSetup } from '../../../../ml/server'; import { SIGNALS_ID, INTERNAL_IMMUTABLE_KEY } from '../../../common/constants'; import { DetectionRulesUsage, MlJobsUsage } from './index'; @@ -164,25 +163,20 @@ export const getRulesUsage = async ( export const getMlJobsUsage = async (ml: MlPluginSetup | undefined): Promise => { let jobsUsage: MlJobsUsage = initialMlJobsUsage; - // Fake objects to be passed to ML functions. - // TODO - These ML functions should come from ML's setup contract - // and not be imported directly. - const fakeScopedClusterClient = { - callAsCurrentUser: ml?.mlClient.callAsInternalUser, - callAsInternalUser: ml?.mlClient.callAsInternalUser, - } as ILegacyScopedClusterClient; - const fakeSavedObjectsClient = {} as SavedObjectsClient; - const fakeRequest = {} as KibanaRequest; - if (ml) { try { - const modules = await new DataRecognizer( - fakeScopedClusterClient, - fakeSavedObjectsClient, - fakeRequest - ).listModules(); + const fakeRequest = { headers: {}, params: 'DummyKibanaRequest' } as KibanaRequest; + const fakeSOClient = {} as SavedObjectsClient; + const internalMlClient = { + callAsCurrentUser: ml?.mlClient.callAsInternalUser, + callAsInternalUser: ml?.mlClient.callAsInternalUser, + }; + + const modules = await ml + .modulesProvider(internalMlClient, fakeRequest, fakeSOClient) + .listModules(); const moduleJobs = modules.flatMap((module) => module.jobs); - const jobs = await jobServiceProvider(fakeScopedClusterClient).jobsSummary(['siem']); + const jobs = await ml.jobServiceProvider(internalMlClient, fakeRequest).jobsSummary(['siem']); jobsUsage = jobs.reduce((usage, job) => { const isElastic = moduleJobs.some((moduleJob) => moduleJob.id === job.id); From b48162b47b01643dbd448a2f7d4032121f0ddc49 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 14 Jul 2020 21:29:42 +0200 Subject: [PATCH 29/82] [SIEM][Timeline] Updates all events text timeline (#71701) * updates 'All events' timeline text to 'All' * updates jest test * fixes test issue --- .../components/timeline/search_or_filter/translations.ts | 2 +- .../public/timelines/components/timeline/timeline.test.tsx | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/translations.ts index 7fa520a2d8df4..b5c78c458697c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/translations.ts @@ -73,7 +73,7 @@ export const FILTER_OR_SEARCH_WITH_KQL = i18n.translate( export const ALL_EVENT = i18n.translate( 'xpack.securitySolution.timeline.searchOrFilter.eventTypeAllEvent', { - defaultMessage: 'All events', + defaultMessage: 'All', } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx index 78a46e04a6952..7711cb7ba620e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx @@ -167,7 +167,7 @@ describe('Timeline', () => { expect(wrapper.find('[data-test-subj="table-pagination"]').exists()).toEqual(false); }); - test('it defaults to showing `All events`', () => { + test('it defaults to showing `All`', () => { const wrapper = mount( @@ -176,9 +176,7 @@ describe('Timeline', () => { ); - expect(wrapper.find('[data-test-subj="pick-event-type"] button').text()).toEqual( - 'All events' - ); + expect(wrapper.find('[data-test-subj="pick-event-type"] button').text()).toEqual('All'); }); it('it shows the timeline footer', () => { From fd1809c3c296505faec09e5b2f52e0dd56f09eaa Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Tue, 14 Jul 2020 15:55:12 -0400 Subject: [PATCH 30/82] [Ingest Manager] Refactor Package Installation (#71521) * refactor installation to add/remove installed assets as they are added/removed * update types * uninstall assets when installation fails * refactor installation to add/remove installed assets as they are added/removed * update types Co-authored-by: Elastic Machine --- .../ingest_manager/common/types/models/epm.ts | 22 +- .../server/routes/data_streams/handlers.ts | 2 +- .../server/routes/epm/handlers.ts | 21 +- .../server/saved_objects/index.ts | 9 +- .../elasticsearch/ingest_pipeline/index.ts | 9 + .../elasticsearch/ingest_pipeline/install.ts | 22 +- .../elasticsearch/ingest_pipeline/remove.ts | 60 +++++ .../epm/elasticsearch/template/install.ts | 26 +- .../epm/elasticsearch/template/template.ts | 15 +- .../services/epm/kibana/assets/install.ts | 126 +++++++++ .../services/epm/packages/get_objects.ts | 32 --- .../server/services/epm/packages/index.ts | 2 +- .../server/services/epm/packages/install.ts | 254 +++++++++--------- .../server/services/epm/packages/remove.ts | 61 ++--- .../ingest_manager/server/types/index.tsx | 3 +- .../store/policy_list/test_mock_utils.ts | 33 +-- 16 files changed, 439 insertions(+), 258 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/index.ts create mode 100644 x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/remove.ts create mode 100644 x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts delete mode 100644 x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index a34038d4fba04..ab6a6c73843c5 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -229,7 +229,8 @@ export type PackageInfo = Installable< >; export interface Installation extends SavedObjectAttributes { - installed: AssetReference[]; + installed_kibana: KibanaAssetReference[]; + installed_es: EsAssetReference[]; es_index_patterns: Record; name: string; version: string; @@ -246,19 +247,14 @@ export type NotInstalled = T & { status: InstallationStatus.notInstalled; }; -export type AssetReference = Pick & { - type: AssetType | IngestAssetType; -}; +export type AssetReference = KibanaAssetReference | EsAssetReference; -/** - * Types of assets which can be installed/removed - */ -export enum IngestAssetType { - IlmPolicy = 'ilm_policy', - IndexTemplate = 'index_template', - ComponentTemplate = 'component_template', - IngestPipeline = 'ingest_pipeline', -} +export type KibanaAssetReference = Pick & { + type: KibanaAssetType; +}; +export type EsAssetReference = Pick & { + type: ElasticsearchAssetType; +}; export enum DefaultPackages { system = 'system', diff --git a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts index 2c65b08a68700..df37aeb27c75c 100644 --- a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts @@ -122,7 +122,7 @@ export const getListHandler: RequestHandler = async (context, request, response) if (pkg !== '' && pkgSavedObject.length > 0 && !packageMetadata[pkg]) { // then pick the dashboards from the package saved object const dashboards = - pkgSavedObject[0].attributes?.installed?.filter( + pkgSavedObject[0].attributes?.installed_kibana?.filter( (o) => o.type === KibanaAssetType.dashboard ) || []; // and then pick the human-readable titles from the dashboard saved objects diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts index fe813f29b72e6..f54e61280b98a 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -5,6 +5,7 @@ */ import { TypeOf } from '@kbn/config-schema'; import { RequestHandler, CustomHttpResponseOptions } from 'src/core/server'; +import { appContextService } from '../../services'; import { GetInfoResponse, InstallPackageResponse, @@ -29,6 +30,7 @@ import { installPackage, removeInstallation, getLimitedPackages, + getInstallationObject, } from '../../services/epm/packages'; export const getCategoriesHandler: RequestHandler< @@ -146,10 +148,12 @@ export const getInfoHandler: RequestHandler> = async (context, request, response) => { + const logger = appContextService.getLogger(); + const savedObjectsClient = context.core.savedObjects.client; + const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; + const { pkgkey } = request.params; + const [pkgName, pkgVersion] = pkgkey.split('-'); try { - const { pkgkey } = request.params; - const savedObjectsClient = context.core.savedObjects.client; - const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; const res = await installPackage({ savedObjectsClient, pkgkey, @@ -161,6 +165,17 @@ export const installPackageHandler: RequestHandler { + // unlike other ES assets, pipeline names are versioned so after a template is updated + // it can be created pointing to the new template, without removing the old one and effecting data + // so do not remove the currently installed pipelines here const datasets = registryPackage.datasets; const pipelinePaths = paths.filter((path) => isPipeline(path)); if (datasets) { - const pipelines = datasets.reduce>>((acc, dataset) => { + const pipelines = datasets.reduce>>((acc, dataset) => { if (dataset.ingest_pipeline) { acc.push( installPipelinesForDataset({ @@ -41,7 +46,8 @@ export const installPipelines = async ( } return acc; }, []); - return Promise.all(pipelines).then((results) => results.flat()); + const pipelinesToSave = await Promise.all(pipelines).then((results) => results.flat()); + return saveInstalledEsRefs(savedObjectsClient, registryPackage.name, pipelinesToSave); } return []; }; @@ -77,7 +83,7 @@ export async function installPipelinesForDataset({ pkgVersion: string; paths: string[]; dataset: Dataset; -}): Promise { +}): Promise { const pipelinePaths = paths.filter((path) => isDatasetPipeline(path, dataset.path)); let pipelines: any[] = []; const substitutions: RewriteSubstitution[] = []; @@ -123,7 +129,7 @@ async function installPipeline({ }: { callCluster: CallESAsCurrentUser; pipeline: any; -}): Promise { +}): Promise { const callClusterParams: { method: string; path: string; @@ -146,7 +152,7 @@ async function installPipeline({ // which we could otherwise use. // See src/core/server/elasticsearch/api_types.ts for available endpoints. await callCluster('transport.request', callClusterParams); - return { id: pipeline.nameForInstallation, type: IngestAssetType.IngestPipeline }; + return { id: pipeline.nameForInstallation, type: ElasticsearchAssetType.ingestPipeline }; } const isDirectory = ({ path }: Registry.ArchiveEntry) => path.endsWith('/'); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/remove.ts new file mode 100644 index 0000000000000..8be3a1beab392 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/remove.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClientContract } from 'src/core/server'; +import { appContextService } from '../../../'; +import { CallESAsCurrentUser, ElasticsearchAssetType } from '../../../../types'; +import { getInstallation } from '../../packages/get'; +import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common'; + +export const deletePipelines = async ( + callCluster: CallESAsCurrentUser, + savedObjectsClient: SavedObjectsClientContract, + pkgName: string, + pkgVersion: string +) => { + const logger = appContextService.getLogger(); + const previousPipelinesPattern = `*-${pkgName}.*-${pkgVersion}`; + + try { + await deletePipeline(callCluster, previousPipelinesPattern); + } catch (e) { + logger.error(e); + } + try { + await deletePipelineRefs(savedObjectsClient, pkgName, pkgVersion); + } catch (e) { + logger.error(e); + } +}; + +export const deletePipelineRefs = async ( + savedObjectsClient: SavedObjectsClientContract, + pkgName: string, + pkgVersion: string +) => { + const installation = await getInstallation({ savedObjectsClient, pkgName }); + if (!installation) return; + const installedEsAssets = installation.installed_es; + const filteredAssets = installedEsAssets.filter(({ type, id }) => { + if (type !== ElasticsearchAssetType.ingestPipeline) return true; + if (!id.includes(pkgVersion)) return true; + return false; + }); + return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { + installed_es: filteredAssets, + }); +}; +export async function deletePipeline(callCluster: CallESAsCurrentUser, id: string): Promise { + // '*' shouldn't ever appear here, but it still would delete all ingest pipelines + if (id && id !== '*') { + try { + await callCluster('ingest.deletePipeline', { id }); + } catch (err) { + throw new Error(`error deleting pipeline ${id}`); + } + } +} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts index e14645bbbf5fb..436a6a1bdc55d 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts @@ -5,6 +5,7 @@ */ import Boom from 'boom'; +import { SavedObjectsClientContract } from 'src/core/server'; import { Dataset, RegistryPackage, @@ -17,13 +18,14 @@ import { Field, loadFieldsFromYaml, processFields } from '../../fields/field'; import { getPipelineNameForInstallation } from '../ingest_pipeline/install'; import { generateMappings, generateTemplateName, getTemplate } from './template'; import * as Registry from '../../registry'; +import { removeAssetsFromInstalledEsByType, saveInstalledEsRefs } from '../../packages/install'; export const installTemplates = async ( registryPackage: RegistryPackage, + isUpdate: boolean, callCluster: CallESAsCurrentUser, - pkgName: string, - pkgVersion: string, - paths: string[] + paths: string[], + savedObjectsClient: SavedObjectsClientContract ): Promise => { // install any pre-built index template assets, // atm, this is only the base package's global index templates @@ -31,6 +33,12 @@ export const installTemplates = async ( await installPreBuiltComponentTemplates(paths, callCluster); await installPreBuiltTemplates(paths, callCluster); + // remove package installation's references to index templates + await removeAssetsFromInstalledEsByType( + savedObjectsClient, + registryPackage.name, + ElasticsearchAssetType.indexTemplate + ); // build templates per dataset from yml files const datasets = registryPackage.datasets; if (datasets) { @@ -46,7 +54,17 @@ export const installTemplates = async ( }, []); const res = await Promise.all(installTemplatePromises); - return res.flat(); + const installedTemplates = res.flat(); + // get template refs to save + const installedTemplateRefs = installedTemplates.map((template) => ({ + id: template.templateName, + type: ElasticsearchAssetType.indexTemplate, + })); + + // add package installation's references to index templates + await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, installedTemplateRefs); + + return installedTemplates; } return []; }; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts index 77ad96952269f..b907c735d2630 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts @@ -326,9 +326,10 @@ export const updateCurrentWriteIndices = async ( callCluster: CallESAsCurrentUser, templates: TemplateRef[] ): Promise => { - if (!templates) return; + if (!templates.length) return; const allIndices = await queryIndicesFromTemplates(callCluster, templates); + if (!allIndices.length) return; return updateAllIndices(allIndices, callCluster); }; @@ -358,12 +359,12 @@ const getIndices = async ( method: 'GET', path: `/_data_stream/${templateName}-*`, }); - if (res.length) { - return res.map((datastream: any) => ({ - indexName: datastream.indices[datastream.indices.length - 1].index_name, - indexTemplate, - })); - } + const dataStreams = res.data_streams; + if (!dataStreams.length) return; + return dataStreams.map((dataStream: any) => ({ + indexName: dataStream.indices[dataStream.indices.length - 1].index_name, + indexTemplate, + })); }; const updateAllIndices = async ( diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts new file mode 100644 index 0000000000000..2a743f244e64d --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + SavedObject, + SavedObjectsBulkCreateObject, + SavedObjectsClientContract, +} from 'src/core/server'; +import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common'; +import * as Registry from '../../registry'; +import { AssetType, KibanaAssetType, AssetReference } from '../../../../types'; +import { deleteKibanaSavedObjectsAssets } from '../../packages/remove'; +import { getInstallationObject, savedObjectTypes } from '../../packages'; +import { saveInstalledKibanaRefs } from '../../packages/install'; + +type SavedObjectToBe = Required & { type: AssetType }; +export type ArchiveAsset = Pick< + SavedObject, + 'id' | 'attributes' | 'migrationVersion' | 'references' +> & { + type: AssetType; +}; + +export async function getKibanaAsset(key: string) { + const buffer = Registry.getAsset(key); + + // cache values are buffers. convert to string / JSON + return JSON.parse(buffer.toString('utf8')); +} + +export function createSavedObjectKibanaAsset(asset: ArchiveAsset): SavedObjectToBe { + // convert that to an object + return { + type: asset.type, + id: asset.id, + attributes: asset.attributes, + references: asset.references || [], + migrationVersion: asset.migrationVersion || {}, + }; +} + +// TODO: make it an exhaustive list +// e.g. switch statement with cases for each enum key returning `never` for default case +export async function installKibanaAssets(options: { + savedObjectsClient: SavedObjectsClientContract; + pkgName: string; + paths: string[]; + isUpdate: boolean; +}): Promise { + const { savedObjectsClient, paths, pkgName, isUpdate } = options; + + if (isUpdate) { + // delete currently installed kibana saved objects and installation references + const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); + const installedKibanaRefs = installedPkg?.attributes.installed_kibana; + + if (installedKibanaRefs?.length) { + await deleteKibanaSavedObjectsAssets(savedObjectsClient, installedKibanaRefs); + await deleteKibanaInstalledRefs(savedObjectsClient, pkgName, installedKibanaRefs); + } + } + + // install the new assets and save installation references + const kibanaAssetTypes = Object.values(KibanaAssetType); + const installationPromises = kibanaAssetTypes.map((assetType) => + installKibanaSavedObjects({ savedObjectsClient, assetType, paths }) + ); + // installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][] + // call .flat to flatten into one dimensional array + const newInstalledKibanaAssets = await Promise.all(installationPromises).then((results) => + results.flat() + ); + await saveInstalledKibanaRefs(savedObjectsClient, pkgName, newInstalledKibanaAssets); + return newInstalledKibanaAssets; +} +export const deleteKibanaInstalledRefs = async ( + savedObjectsClient: SavedObjectsClientContract, + pkgName: string, + installedKibanaRefs: AssetReference[] +) => { + const installedAssetsToSave = installedKibanaRefs.filter(({ id, type }) => { + const assetType = type as AssetType; + return !savedObjectTypes.includes(assetType); + }); + + return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { + installed_kibana: installedAssetsToSave, + }); +}; + +async function installKibanaSavedObjects({ + savedObjectsClient, + assetType, + paths, +}: { + savedObjectsClient: SavedObjectsClientContract; + assetType: KibanaAssetType; + paths: string[]; +}) { + const isSameType = (path: string) => assetType === Registry.pathParts(path).type; + const pathsOfType = paths.filter((path) => isSameType(path)); + const kibanaAssets = await Promise.all(pathsOfType.map((path) => getKibanaAsset(path))); + const toBeSavedObjects = await Promise.all( + kibanaAssets.map((asset) => createSavedObjectKibanaAsset(asset)) + ); + + if (toBeSavedObjects.length === 0) { + return []; + } else { + const createResults = await savedObjectsClient.bulkCreate(toBeSavedObjects, { + overwrite: true, + }); + const createdObjects = createResults.saved_objects; + const installed = createdObjects.map(toAssetReference); + return installed; + } +} + +function toAssetReference({ id, type }: SavedObject) { + const reference: AssetReference = { id, type: type as KibanaAssetType }; + + return reference; +} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts deleted file mode 100644 index b623295c5e060..0000000000000 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { SavedObject, SavedObjectsBulkCreateObject } from 'src/core/server'; -import { AssetType } from '../../../types'; -import * as Registry from '../registry'; - -type ArchiveAsset = Pick; -type SavedObjectToBe = Required & { type: AssetType }; - -export async function getObject(key: string) { - const buffer = Registry.getAsset(key); - - // cache values are buffers. convert to string / JSON - const json = buffer.toString('utf8'); - // convert that to an object - const asset: ArchiveAsset = JSON.parse(json); - - const { type, file } = Registry.pathParts(key); - const savedObject: SavedObjectToBe = { - type, - id: file.replace('.json', ''), - attributes: asset.attributes, - references: asset.references || [], - migrationVersion: asset.migrationVersion || {}, - }; - - return savedObject; -} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts index 4bb803dfaf912..57c4f77432455 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts @@ -23,7 +23,7 @@ export { SearchParams, } from './get'; -export { installKibanaAssets, installPackage, ensureInstalledPackage } from './install'; +export { installPackage, ensureInstalledPackage } from './install'; export { removeInstallation } from './remove'; type RequiredPackage = 'system' | 'endpoint'; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 910283549abdf..35c5b58a93710 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -4,27 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; +import { SavedObjectsClientContract } from 'src/core/server'; import Boom from 'boom'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { AssetReference, Installation, - KibanaAssetType, CallESAsCurrentUser, DefaultPackages, + AssetType, + KibanaAssetReference, + EsAssetReference, ElasticsearchAssetType, - IngestAssetType, } from '../../../types'; import { installIndexPatterns } from '../kibana/index_pattern/install'; import * as Registry from '../registry'; -import { getObject } from './get_objects'; import { getInstallation, getInstallationObject, isRequiredPackage } from './index'; import { installTemplates } from '../elasticsearch/template/install'; import { generateESIndexPatterns } from '../elasticsearch/template/template'; -import { installPipelines } from '../elasticsearch/ingest_pipeline/install'; +import { installPipelines, deletePipelines } from '../elasticsearch/ingest_pipeline/'; import { installILMPolicy } from '../elasticsearch/ilm/install'; -import { deleteAssetsByType, deleteKibanaSavedObjectsAssets } from './remove'; +import { installKibanaAssets } from '../kibana/assets/install'; import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; export async function installLatestPackage(options: { @@ -92,127 +92,113 @@ export async function installPackage(options: { const { savedObjectsClient, pkgkey, callCluster } = options; // TODO: change epm API to /packageName/version so we don't need to do this const [pkgName, pkgVersion] = pkgkey.split('-'); - const paths = await Registry.getArchiveInfo(pkgName, pkgVersion); - // see if some version of this package is already installed // TODO: calls to getInstallationObject, Registry.fetchInfo, and Registry.fetchFindLatestPackge // and be replaced by getPackageInfo after adjusting for it to not group/use archive assets - const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); - const registryPackageInfo = await Registry.fetchInfo(pkgName, pkgVersion); const latestPackage = await Registry.fetchFindLatestPackage(pkgName); if (pkgVersion < latestPackage.version) throw Boom.badRequest('Cannot install or update to an out-of-date package'); + const paths = await Registry.getArchiveInfo(pkgName, pkgVersion); + const registryPackageInfo = await Registry.fetchInfo(pkgName, pkgVersion); + + // get the currently installed package + const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); + const isUpdate = installedPkg && installedPkg.attributes.version < pkgVersion ? true : false; + const reinstall = pkgVersion === installedPkg?.attributes.version; const removable = !isRequiredPackage(pkgName); const { internal = false } = registryPackageInfo; + const toSaveESIndexPatterns = generateESIndexPatterns(registryPackageInfo.datasets); - // delete the previous version's installation's SO kibana assets before installing new ones - // in case some assets were removed in the new version - if (installedPkg) { - try { - await deleteKibanaSavedObjectsAssets(savedObjectsClient, installedPkg.attributes.installed); - } catch (err) { - // log these errors, some assets may not exist if deleted during a failed update - } - } - - const [installedKibanaAssets, installedPipelines] = await Promise.all([ - installKibanaAssets({ + // add the package installation to the saved object + if (!installedPkg) { + await createInstallation({ savedObjectsClient, pkgName, pkgVersion, - paths, - }), - installPipelines(registryPackageInfo, paths, callCluster), - // index patterns and ilm policies are not currently associated with a particular package - // so we do not save them in the package saved object state. - installIndexPatterns(savedObjectsClient, pkgName, pkgVersion), - // currenly only the base package has an ILM policy - // at some point ILM policies can be installed/modified - // per dataset and we should then save them - installILMPolicy(paths, callCluster), - ]); + internal, + removable, + installed_kibana: [], + installed_es: [], + toSaveESIndexPatterns, + }); + } - // install or update the templates + const installIndexPatternPromise = installIndexPatterns(savedObjectsClient, pkgName, pkgVersion); + const installKibanaAssetsPromise = installKibanaAssets({ + savedObjectsClient, + pkgName, + paths, + isUpdate, + }); + + // the rest of the installation must happen in sequential order + + // currently only the base package has an ILM policy + // at some point ILM policies can be installed/modified + // per dataset and we should then save them + await installILMPolicy(paths, callCluster); + + // installs versionized pipelines without removing currently installed ones + const installedPipelines = await installPipelines( + registryPackageInfo, + paths, + callCluster, + savedObjectsClient + ); + // install or update the templates referencing the newly installed pipelines const installedTemplates = await installTemplates( registryPackageInfo, + isUpdate, callCluster, - pkgName, - pkgVersion, - paths + paths, + savedObjectsClient ); - const toSaveESIndexPatterns = generateESIndexPatterns(registryPackageInfo.datasets); + // update current backing indices of each data stream + await updateCurrentWriteIndices(callCluster, installedTemplates); + + // if this is an update, delete the previous version's pipelines + if (installedPkg && !reinstall) { + await deletePipelines( + callCluster, + savedObjectsClient, + pkgName, + installedPkg.attributes.version + ); + } + + // update to newly installed version when all assets are successfully installed + if (isUpdate) await updateVersion(savedObjectsClient, pkgName, pkgVersion); // get template refs to save const installedTemplateRefs = installedTemplates.map((template) => ({ id: template.templateName, - type: IngestAssetType.IndexTemplate, + type: ElasticsearchAssetType.indexTemplate, })); - - if (installedPkg) { - // update current index for every index template created - await updateCurrentWriteIndices(callCluster, installedTemplates); - if (!reinstall) { - try { - // delete the previous version's installation's pipelines - // this must happen after the template is updated - await deleteAssetsByType({ - savedObjectsClient, - callCluster, - installedObjects: installedPkg.attributes.installed, - assetType: ElasticsearchAssetType.ingestPipeline, - }); - } catch (err) { - throw new Error(err.message); - } - } - } - const toSaveAssetRefs: AssetReference[] = [ - ...installedKibanaAssets, - ...installedPipelines, - ...installedTemplateRefs, - ]; - // Save references to installed assets in the package's saved object state - return saveInstallationReferences({ - savedObjectsClient, - pkgName, - pkgVersion, - internal, - removable, - toSaveAssetRefs, - toSaveESIndexPatterns, - }); -} - -// TODO: make it an exhaustive list -// e.g. switch statement with cases for each enum key returning `never` for default case -export async function installKibanaAssets(options: { - savedObjectsClient: SavedObjectsClientContract; - pkgName: string; - pkgVersion: string; - paths: string[]; -}) { - const { savedObjectsClient, paths } = options; - - // Only install Kibana assets during package installation. - const kibanaAssetTypes = Object.values(KibanaAssetType); - const installationPromises = kibanaAssetTypes.map(async (assetType) => - installKibanaSavedObjects({ savedObjectsClient, assetType, paths }) - ); - - // installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][] - // call .flat to flatten into one dimensional array - return Promise.all(installationPromises).then((results) => results.flat()); + const [installedKibanaAssets] = await Promise.all([ + installKibanaAssetsPromise, + installIndexPatternPromise, + ]); + return [...installedKibanaAssets, ...installedPipelines, ...installedTemplateRefs]; } - -export async function saveInstallationReferences(options: { +const updateVersion = async ( + savedObjectsClient: SavedObjectsClientContract, + pkgName: string, + pkgVersion: string +) => { + return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { + version: pkgVersion, + }); +}; +export async function createInstallation(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; pkgVersion: string; internal: boolean; removable: boolean; - toSaveAssetRefs: AssetReference[]; + installed_kibana: KibanaAssetReference[]; + installed_es: EsAssetReference[]; toSaveESIndexPatterns: Record; }) { const { @@ -221,14 +207,15 @@ export async function saveInstallationReferences(options: { pkgVersion, internal, removable, - toSaveAssetRefs, + installed_kibana: installedKibana, + installed_es: installedEs, toSaveESIndexPatterns, } = options; - await savedObjectsClient.create( PACKAGES_SAVED_OBJECT_TYPE, { - installed: toSaveAssetRefs, + installed_kibana: installedKibana, + installed_es: installedEs, es_index_patterns: toSaveESIndexPatterns, name: pkgName, version: pkgVersion, @@ -237,37 +224,46 @@ export async function saveInstallationReferences(options: { }, { id: pkgName, overwrite: true } ); - - return toSaveAssetRefs; + return [...installedKibana, ...installedEs]; } -async function installKibanaSavedObjects({ - savedObjectsClient, - assetType, - paths, -}: { - savedObjectsClient: SavedObjectsClientContract; - assetType: KibanaAssetType; - paths: string[]; -}) { - const isSameType = (path: string) => assetType === Registry.pathParts(path).type; - const pathsOfType = paths.filter((path) => isSameType(path)); - const toBeSavedObjects = await Promise.all(pathsOfType.map(getObject)); +export const saveInstalledKibanaRefs = async ( + savedObjectsClient: SavedObjectsClientContract, + pkgName: string, + installedAssets: AssetReference[] +) => { + await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { + installed_kibana: installedAssets, + }); + return installedAssets; +}; - if (toBeSavedObjects.length === 0) { - return []; - } else { - const createResults = await savedObjectsClient.bulkCreate(toBeSavedObjects, { - overwrite: true, - }); - const createdObjects = createResults.saved_objects; - const installed = createdObjects.map(toAssetReference); - return installed; - } -} +export const saveInstalledEsRefs = async ( + savedObjectsClient: SavedObjectsClientContract, + pkgName: string, + installedAssets: EsAssetReference[] +) => { + const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); + const installedAssetsToSave = installedPkg?.attributes.installed_es.concat(installedAssets); + await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { + installed_es: installedAssetsToSave, + }); + return installedAssets; +}; -function toAssetReference({ id, type }: SavedObject) { - const reference: AssetReference = { id, type: type as KibanaAssetType }; +export const removeAssetsFromInstalledEsByType = async ( + savedObjectsClient: SavedObjectsClientContract, + pkgName: string, + assetType: AssetType +) => { + const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); + const installedAssets = installedPkg?.attributes.installed_es; + if (!installedAssets?.length) return; + const installedAssetsToSave = installedAssets?.filter(({ id, type }) => { + return type !== assetType; + }); - return reference; -} + return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { + installed_es: installedAssetsToSave, + }); +}; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts index 94af672d8e29f..81bc5847e6c0e 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts @@ -10,8 +10,9 @@ import { PACKAGES_SAVED_OBJECT_TYPE, PACKAGE_CONFIG_SAVED_OBJECT_TYPE } from '.. import { AssetReference, AssetType, ElasticsearchAssetType } from '../../../types'; import { CallESAsCurrentUser } from '../../../types'; import { getInstallation, savedObjectTypes } from './index'; +import { deletePipeline } from '../elasticsearch/ingest_pipeline/'; import { installIndexPatterns } from '../kibana/index_pattern/install'; -import { packageConfigService } from '../..'; +import { packageConfigService, appContextService } from '../..'; export async function removeInstallation(options: { savedObjectsClient: SavedObjectsClientContract; @@ -25,7 +26,6 @@ export async function removeInstallation(options: { if (!installation) throw Boom.badRequest(`${pkgName} is not installed`); if (installation.removable === false) throw Boom.badRequest(`${pkgName} is installed by default and cannot be removed`); - const installedObjects = installation.installed || []; const { total } = await packageConfigService.list(savedObjectsClient, { kuery: `${PACKAGE_CONFIG_SAVED_OBJECT_TYPE}.package.name:${pkgName}`, @@ -38,48 +38,40 @@ export async function removeInstallation(options: { `unable to remove package with existing package config(s) in use by agent(s)` ); - // Delete the manager saved object with references to the asset objects - // could also update with [] or some other state - await savedObjectsClient.delete(PACKAGES_SAVED_OBJECT_TYPE, pkgName); - // recreate or delete index patterns when a package is uninstalled await installIndexPatterns(savedObjectsClient); - // Delete the installed asset - await deleteAssets(installedObjects, savedObjectsClient, callCluster); + // Delete the installed assets + const installedAssets = [...installation.installed_kibana, ...installation.installed_es]; + await deleteAssets(installedAssets, savedObjectsClient, callCluster); + + // Delete the manager saved object with references to the asset objects + // could also update with [] or some other state + await savedObjectsClient.delete(PACKAGES_SAVED_OBJECT_TYPE, pkgName); // successful delete's in SO client return {}. return something more useful - return installedObjects; + return installedAssets; } async function deleteAssets( installedObjects: AssetReference[], savedObjectsClient: SavedObjectsClientContract, callCluster: CallESAsCurrentUser ) { + const logger = appContextService.getLogger(); const deletePromises = installedObjects.map(async ({ id, type }) => { const assetType = type as AssetType; if (savedObjectTypes.includes(assetType)) { - savedObjectsClient.delete(assetType, id); + return savedObjectsClient.delete(assetType, id); } else if (assetType === ElasticsearchAssetType.ingestPipeline) { - deletePipeline(callCluster, id); + return deletePipeline(callCluster, id); } else if (assetType === ElasticsearchAssetType.indexTemplate) { - deleteTemplate(callCluster, id); + return deleteTemplate(callCluster, id); } }); try { await Promise.all([...deletePromises]); } catch (err) { - throw new Error(err.message); - } -} -async function deletePipeline(callCluster: CallESAsCurrentUser, id: string): Promise { - // '*' shouldn't ever appear here, but it still would delete all ingest pipelines - if (id && id !== '*') { - try { - await callCluster('ingest.deletePipeline', { id }); - } catch (err) { - throw new Error(`error deleting pipeline ${id}`); - } + logger.error(err); } } @@ -108,31 +100,14 @@ async function deleteTemplate(callCluster: CallESAsCurrentUser, name: string): P } } -export async function deleteAssetsByType({ - savedObjectsClient, - callCluster, - installedObjects, - assetType, -}: { - savedObjectsClient: SavedObjectsClientContract; - callCluster: CallESAsCurrentUser; - installedObjects: AssetReference[]; - assetType: ElasticsearchAssetType; -}) { - const toDelete = installedObjects.filter((asset) => asset.type === assetType); - try { - await deleteAssets(toDelete, savedObjectsClient, callCluster); - } catch (err) { - throw new Error(err.message); - } -} - export async function deleteKibanaSavedObjectsAssets( savedObjectsClient: SavedObjectsClientContract, installedObjects: AssetReference[] ) { + const logger = appContextService.getLogger(); const deletePromises = installedObjects.map(({ id, type }) => { const assetType = type as AssetType; + if (savedObjectTypes.includes(assetType)) { return savedObjectsClient.delete(assetType, id); } @@ -140,6 +115,6 @@ export async function deleteKibanaSavedObjectsAssets( try { await Promise.all(deletePromises); } catch (err) { - throw new Error('error deleting saved object asset'); + logger.warn(err); } } diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx index a559ca18cfede..5d0683a37dc5e 100644 --- a/x-pack/plugins/ingest_manager/server/types/index.tsx +++ b/x-pack/plugins/ingest_manager/server/types/index.tsx @@ -43,8 +43,9 @@ export { Dataset, RegistryElasticsearch, AssetReference, + EsAssetReference, + KibanaAssetReference, ElasticsearchAssetType, - IngestAssetType, RegistryPackage, AssetType, Installable, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts index 963b7922a7bff..b5c67cc2c2014 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts @@ -9,7 +9,8 @@ import { INGEST_API_PACKAGE_CONFIGS, INGEST_API_EPM_PACKAGES } from './services/ import { EndpointDocGenerator } from '../../../../../../common/endpoint/generate_data'; import { GetPolicyListResponse } from '../../types'; import { - AssetReference, + KibanaAssetReference, + EsAssetReference, GetPackagesResponse, InstallationStatus, } from '../../../../../../../ingest_manager/common'; @@ -43,26 +44,28 @@ export const apiPathMockResponseProviders = { type: 'epm-packages', id: 'endpoint', attributes: { - installed: [ + installed_kibana: [ { id: '826759f0-7074-11ea-9bc8-6b38f4d29a16', type: 'dashboard' }, { id: '1cfceda0-728b-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, { id: '1e525190-7074-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, { id: '55387750-729c-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, { id: '92b1edc0-706a-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, { id: 'a3a3bd10-706b-11ea-9bc8-6b38f4d29a16', type: 'map' }, - { id: 'logs-endpoint.alerts', type: 'index-template' }, - { id: 'events-endpoint', type: 'index-template' }, - { id: 'logs-endpoint.events.file', type: 'index-template' }, - { id: 'logs-endpoint.events.library', type: 'index-template' }, - { id: 'metrics-endpoint.metadata', type: 'index-template' }, - { id: 'metrics-endpoint.metadata_mirror', type: 'index-template' }, - { id: 'logs-endpoint.events.network', type: 'index-template' }, - { id: 'metrics-endpoint.policy', type: 'index-template' }, - { id: 'logs-endpoint.events.process', type: 'index-template' }, - { id: 'logs-endpoint.events.registry', type: 'index-template' }, - { id: 'logs-endpoint.events.security', type: 'index-template' }, - { id: 'metrics-endpoint.telemetry', type: 'index-template' }, - ] as AssetReference[], + ] as KibanaAssetReference[], + installed_es: [ + { id: 'logs-endpoint.alerts', type: 'index_template' }, + { id: 'events-endpoint', type: 'index_template' }, + { id: 'logs-endpoint.events.file', type: 'index_template' }, + { id: 'logs-endpoint.events.library', type: 'index_template' }, + { id: 'metrics-endpoint.metadata', type: 'index_template' }, + { id: 'metrics-endpoint.metadata_mirror', type: 'index_template' }, + { id: 'logs-endpoint.events.network', type: 'index_template' }, + { id: 'metrics-endpoint.policy', type: 'index_template' }, + { id: 'logs-endpoint.events.process', type: 'index_template' }, + { id: 'logs-endpoint.events.registry', type: 'index_template' }, + { id: 'logs-endpoint.events.security', type: 'index_template' }, + { id: 'metrics-endpoint.telemetry', type: 'index_template' }, + ] as EsAssetReference[], es_index_patterns: { alerts: 'logs-endpoint.alerts-*', events: 'events-endpoint-*', From 0b675b89084b18faa1db1ca99ecd500a78af8f57 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Tue, 14 Jul 2020 14:59:21 -0500 Subject: [PATCH 31/82] [DOCS] Fixes to API docs (#71678) * [DOCS] Fixes to API docs * Fixes rogue -u --- docs/api/dashboard/export-dashboard.asciidoc | 2 +- docs/api/dashboard/import-dashboard.asciidoc | 2 +- .../create-logstash.asciidoc | 2 +- .../delete-pipeline.asciidoc | 2 +- docs/api/role-management/put.asciidoc | 10 +++++----- docs/api/saved-objects/bulk_create.asciidoc | 2 +- docs/api/saved-objects/bulk_get.asciidoc | 2 +- docs/api/saved-objects/create.asciidoc | 2 +- docs/api/saved-objects/delete.asciidoc | 2 +- docs/api/saved-objects/export.asciidoc | 8 ++++---- docs/api/saved-objects/find.asciidoc | 4 ++-- docs/api/saved-objects/get.asciidoc | 4 ++-- docs/api/saved-objects/import.asciidoc | 6 +++--- .../resolve_import_errors.asciidoc | 6 +++--- docs/api/saved-objects/update.asciidoc | 2 +- .../copy_saved_objects.asciidoc | 4 ++-- docs/api/spaces-management/post.asciidoc | 2 +- docs/api/spaces-management/put.asciidoc | 2 +- ...olve_copy_saved_objects_conflicts.asciidoc | 2 +- .../batch_reindexing.asciidoc | 6 ++++-- .../check_reindex_status.asciidoc | 1 + docs/api/url-shortening.asciidoc | 19 ++++++++++++------- docs/api/using-api.asciidoc | 2 +- 23 files changed, 51 insertions(+), 43 deletions(-) diff --git a/docs/api/dashboard/export-dashboard.asciidoc b/docs/api/dashboard/export-dashboard.asciidoc index 36c551dee84fc..2099fb599ba67 100644 --- a/docs/api/dashboard/export-dashboard.asciidoc +++ b/docs/api/dashboard/export-dashboard.asciidoc @@ -35,7 +35,7 @@ experimental[] Export dashboards and corresponding saved objects. [source,sh] -------------------------------------------------- -$ curl -X GET "localhost:5601/api/kibana/dashboards/export?dashboard=942dcef0-b2cd-11e8-ad8e-85441f0c2e5c" <1> +$ curl -X GET api/kibana/dashboards/export?dashboard=942dcef0-b2cd-11e8-ad8e-85441f0c2e5c <1> -------------------------------------------------- // KIBANA diff --git a/docs/api/dashboard/import-dashboard.asciidoc b/docs/api/dashboard/import-dashboard.asciidoc index 320859f78c617..020ec8018b85b 100644 --- a/docs/api/dashboard/import-dashboard.asciidoc +++ b/docs/api/dashboard/import-dashboard.asciidoc @@ -42,7 +42,7 @@ Use the complete response body from the < "index1", @@ -40,7 +40,9 @@ POST /api/upgrade_assistant/reindex/batch ] } -------------------------------------------------- -<1> The order in which the indices are provided here determines the order in which the reindex tasks will be executed. +// KIBANA + +<1> The order of the indices determines the order that the reindex tasks are executed. Similar to the <>, the API returns the following: diff --git a/docs/api/upgrade-assistant/check_reindex_status.asciidoc b/docs/api/upgrade-assistant/check_reindex_status.asciidoc index 00801f201d1e1..98cf263673f73 100644 --- a/docs/api/upgrade-assistant/check_reindex_status.asciidoc +++ b/docs/api/upgrade-assistant/check_reindex_status.asciidoc @@ -64,6 +64,7 @@ The API returns the following: `3`:: Paused ++ NOTE: If the {kib} node that started the reindex is shutdown or restarted, the reindex goes into a paused state after some time. To resume the reindex, you must submit a new POST request to the `/api/upgrade_assistant/reindex/` endpoint. diff --git a/docs/api/url-shortening.asciidoc b/docs/api/url-shortening.asciidoc index a62529e11a9ba..ffe1d925e5dcb 100644 --- a/docs/api/url-shortening.asciidoc +++ b/docs/api/url-shortening.asciidoc @@ -1,5 +1,5 @@ [[url-shortening-api]] -=== Shorten URL API +== Shorten URL API ++++ Shorten URL ++++ @@ -9,34 +9,39 @@ Internet Explorer has URL length restrictions, and some wiki and markup parsers Short URLs are designed to make sharing {kib} URLs easier. +[float] [[url-shortening-api-request]] -==== Request +=== Request `POST :/api/shorten_url` +[float] [[url-shortening-api-request-body]] -==== Request body +=== Request body `url`:: (Required, string) The {kib} URL that you want to shorten, relative to `/app/kibana`. +[float] [[url-shortening-api-response-body]] -==== Response body +=== Response body urlId:: A top-level property that contains the shortened URL token for the provided request body. +[float] [[url-shortening-api-codes]] -==== Response code +=== Response code `200`:: Indicates a successful call. +[float] [[url-shortening-api-example]] -==== Example +=== Example [source,sh] -------------------------------------------------- -$ curl -X POST "localhost:5601/api/shorten_url" +$ curl -X POST api/shorten_url { "url": "/app/kibana#/dashboard?_g=()&_a=(description:'',filters:!(),fullScreenMode:!f,options:(hidePanelTitles:!f,useMargins:!t),panels:!((embeddableConfig:(),gridData:(h:15,i:'1',w:24,x:0,y:0),id:'8f4d0c00-4c86-11e8-b3d7-01146121b73d',panelIndex:'1',type:visualization,version:'7.0.0-alpha1')),query:(language:lucene,query:''),timeRestore:!f,title:'New%20Dashboard',viewMode:edit)" } diff --git a/docs/api/using-api.asciidoc b/docs/api/using-api.asciidoc index e58d9c39ee8c4..188c8f9a5909d 100644 --- a/docs/api/using-api.asciidoc +++ b/docs/api/using-api.asciidoc @@ -31,7 +31,7 @@ For example, the following `curl` command exports a dashboard: [source,sh] -- -curl -X POST -u $USER:$PASSWORD "localhost:5601/api/kibana/dashboards/export?dashboard=942dcef0-b2cd-11e8-ad8e-85441f0c2e5c" +curl -X POST api/kibana/dashboards/export?dashboard=942dcef0-b2cd-11e8-ad8e-85441f0c2e5c -- // KIBANA From debcdbac3341cc9f8278d035926de505e79e38ec Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Tue, 14 Jul 2020 13:01:12 -0700 Subject: [PATCH 32/82] Fix mappings for Upgrade Assistant reindexOperationSavedObjectType. (#71710) --- .../reindex_operation_saved_object_type.ts | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/upgrade_assistant/server/saved_object_types/reindex_operation_saved_object_type.ts b/x-pack/plugins/upgrade_assistant/server/saved_object_types/reindex_operation_saved_object_type.ts index ba661fbeceb26..d8976cf19f7e8 100644 --- a/x-pack/plugins/upgrade_assistant/server/saved_object_types/reindex_operation_saved_object_type.ts +++ b/x-pack/plugins/upgrade_assistant/server/saved_object_types/reindex_operation_saved_object_type.ts @@ -15,13 +15,25 @@ export const reindexOperationSavedObjectType: SavedObjectsType = { mappings: { properties: { reindexTaskId: { - type: 'keyword', + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 256, + }, + }, }, indexName: { type: 'keyword', }, newIndexName: { - type: 'keyword', + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 256, + }, + }, }, status: { type: 'integer', @@ -30,10 +42,19 @@ export const reindexOperationSavedObjectType: SavedObjectsType = { type: 'date', }, lastCompletedStep: { - type: 'integer', + type: 'long', }, + // Note that reindex failures can result in extremely long error messages coming from ES. + // We need to map these errors as text and use ignore_above to prevent indexing really large + // messages as keyword. See https://github.com/elastic/kibana/issues/71642 for more info. errorMessage: { - type: 'keyword', + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 256, + }, + }, }, reindexTaskPercComplete: { type: 'float', From 6d5a18732c022dd56441c1eb0d94d3e0ad786f84 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 14 Jul 2020 22:17:50 +0200 Subject: [PATCH 33/82] removes timeline callout (#71718) --- .../timelines/components/open_timeline/open_timeline.tsx | 3 +-- .../timelines/components/open_timeline/translations.ts | 8 -------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx index 60b009f59c13b..13786c55e2a8d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiPanel, EuiBasicTable, EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { EuiPanel, EuiBasicTable, EuiSpacer } from '@elastic/eui'; import React, { useCallback, useMemo, useRef } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -183,7 +183,6 @@ export const OpenTimeline = React.memo( /> - {!!timelineFilter && timelineFilter} Date: Tue, 14 Jul 2020 22:39:44 +0200 Subject: [PATCH 34/82] [Uptime] Visitors breakdowns and enable rum view only via URL (#71428) Co-authored-by: Elastic Machine --- .../cypress/integration/rum_dashboard.feature | 24 ++--- .../apm/e2e/cypress/integration/snapshots.js | 16 ---- .../step_definitions/rum/page_load_dist.ts | 4 +- .../step_definitions/rum/rum_dashboard.ts | 36 +++---- .../rum/service_name_filter.ts | 6 +- .../apm/public/components/app/Home/index.tsx | 16 +--- .../app/Main/route_config/index.tsx | 14 +-- .../Breakdowns/BreakdownGroup.tsx | 1 + .../Charts/VisitorBreakdownChart.tsx | 96 +++++++++++++++++++ .../app/RumDashboard/ClientMetrics/index.tsx | 1 + .../PageLoadDistribution/index.tsx | 1 + .../app/RumDashboard/PageViewsTrend/index.tsx | 1 + .../app/RumDashboard/RumDashboard.tsx | 13 ++- .../app/RumDashboard/RumHeader/index.tsx | 20 ++++ .../components/app/RumDashboard/RumHome.tsx | 27 ++++++ .../RumDashboard/VisitorBreakdown/index.tsx | 65 +++++++++++++ .../components/app/RumDashboard/index.tsx | 27 +++--- .../app/RumDashboard/translations.ts | 7 ++ .../app/ServiceDetails/ServiceDetailTabs.tsx | 24 +---- .../components/shared/KueryBar/index.tsx | 2 +- .../shared/Links/apm/RumOverviewLink.tsx | 27 ------ .../ServiceNameFilter/index.tsx | 4 +- .../context/UrlParamsContext/helpers.ts | 1 - .../lib/rum_client/get_visitor_breakdown.ts | 77 +++++++++++++++ .../apm/server/routes/create_apm_api.ts | 2 + .../plugins/apm/server/routes/rum_client.ts | 13 +++ 26 files changed, 373 insertions(+), 152 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/RumHeader/index.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx delete mode 100644 x-pack/plugins/apm/public/components/shared/Links/apm/RumOverviewLink.tsx create mode 100644 x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts diff --git a/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature b/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature index c98e3f81b2bc6..be1597c8340eb 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature +++ b/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature @@ -1,10 +1,8 @@ Feature: RUM Dashboard Scenario: Client metrics - Given a user browses the APM UI application for RUM Data - When the user inspects the real user monitoring tab - Then should redirect to rum dashboard - And should have correct client metrics + When a user browses the APM UI application for RUM Data + Then should have correct client metrics Scenario Outline: Rum page filters When the user filters by "" @@ -15,22 +13,16 @@ Feature: RUM Dashboard | location | Scenario: Page load distribution percentiles - Given a user browses the APM UI application for RUM Data - When the user inspects the real user monitoring tab - Then should redirect to rum dashboard - And should display percentile for page load chart + When a user browses the APM UI application for RUM Data + Then should display percentile for page load chart Scenario: Page load distribution chart tooltip - Given a user browses the APM UI application for RUM Data - When the user inspects the real user monitoring tab - Then should redirect to rum dashboard - And should display tooltip on hover + When a user browses the APM UI application for RUM Data + Then should display tooltip on hover Scenario: Page load distribution chart legends - Given a user browses the APM UI application for RUM Data - When the user inspects the real user monitoring tab - Then should redirect to rum dashboard - And should display chart legend + When a user browses the APM UI application for RUM Data + Then should display chart legend Scenario: Breakdown filter Given a user click page load breakdown filter diff --git a/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js index 7fbce2583903c..6ee204781c8a7 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js +++ b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js @@ -1,11 +1,6 @@ module.exports = { "__version": "4.9.0", "RUM Dashboard": { - "Client metrics": { - "1": "55 ", - "2": "0.08 sec", - "3": "0.01 sec" - }, "Rum page filters (example #1)": { "1": "8 ", "2": "0.08 sec", @@ -16,19 +11,8 @@ module.exports = { "2": "0.07 sec", "3": "0.01 sec" }, - "Page load distribution percentiles": { - "1": "50th", - "2": "75th", - "3": "90th", - "4": "95th" - }, "Page load distribution chart legends": { "1": "Overall" - }, - "Service name filter": { - "1": "7 ", - "2": "0.07 sec", - "3": "0.01 sec" } } } diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/page_load_dist.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/page_load_dist.ts index 89dc3437c3e69..f319f7ef98667 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/page_load_dist.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/page_load_dist.ts @@ -27,7 +27,9 @@ When(`the user selected the breakdown`, () => { Then(`breakdown series should appear in chart`, () => { cy.get('.euiLoadingChart').should('not.be.visible'); - cy.get('div.echLegendItem__label[title=Chrome] ') + cy.get('div.echLegendItem__label[title=Chrome] ', { + timeout: DEFAULT_TIMEOUT, + }) .invoke('text') .should('eq', 'Chrome'); }); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_dashboard.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_dashboard.ts index 24961ceb3b3c2..ac7aaf33b7849 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_dashboard.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_dashboard.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'; +import { Given, Then } from 'cypress-cucumber-preprocessor/steps'; import { loginAndWaitForPage } from '../../../integration/helpers'; /** The default time in ms to wait for a Cypress command to complete */ @@ -14,18 +14,10 @@ Given(`a user browses the APM UI application for RUM Data`, () => { // open service overview page const RANGE_FROM = 'now-24h'; const RANGE_TO = 'now'; - loginAndWaitForPage(`/app/apm#/services`, { from: RANGE_FROM, to: RANGE_TO }); -}); - -When(`the user inspects the real user monitoring tab`, () => { - // click rum tab - cy.get(':contains(Real User Monitoring)', { timeout: DEFAULT_TIMEOUT }) - .last() - .click({ force: true }); -}); - -Then(`should redirect to rum dashboard`, () => { - cy.url().should('contain', `/app/apm#/rum-overview`); + loginAndWaitForPage(`/app/apm#/rum-preview`, { + from: RANGE_FROM, + to: RANGE_TO, + }); }); Then(`should have correct client metrics`, () => { @@ -33,31 +25,33 @@ Then(`should have correct client metrics`, () => { // wait for all loading to finish cy.get('kbnLoadingIndicator').should('not.be.visible'); + cy.get('.euiStat__title', { timeout: DEFAULT_TIMEOUT }).should('be.visible'); + cy.get('.euiSelect-isLoading').should('not.be.visible'); cy.get('.euiStat__title-isLoading').should('not.be.visible'); - cy.get(clientMetrics).eq(2).invoke('text').snapshot(); + cy.get(clientMetrics).eq(2).should('have.text', '55 '); - cy.get(clientMetrics).eq(1).invoke('text').snapshot(); + cy.get(clientMetrics).eq(1).should('have.text', '0.08 sec'); - cy.get(clientMetrics).eq(0).invoke('text').snapshot(); + cy.get(clientMetrics).eq(0).should('have.text', '0.01 sec'); }); Then(`should display percentile for page load chart`, () => { const pMarkers = '[data-cy=percentile-markers] span'; - cy.get('.euiLoadingChart').should('be.visible'); + cy.get('.euiLoadingChart', { timeout: DEFAULT_TIMEOUT }).should('be.visible'); // wait for all loading to finish cy.get('kbnLoadingIndicator').should('not.be.visible'); cy.get('.euiStat__title-isLoading').should('not.be.visible'); - cy.get(pMarkers).eq(0).invoke('text').snapshot(); + cy.get(pMarkers).eq(0).should('have.text', '50th'); - cy.get(pMarkers).eq(1).invoke('text').snapshot(); + cy.get(pMarkers).eq(1).should('have.text', '75th'); - cy.get(pMarkers).eq(2).invoke('text').snapshot(); + cy.get(pMarkers).eq(2).should('have.text', '90th'); - cy.get(pMarkers).eq(3).invoke('text').snapshot(); + cy.get(pMarkers).eq(3).should('have.text', '95th'); }); Then(`should display chart legend`, () => { diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/service_name_filter.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/service_name_filter.ts index 9a3d7b52674b7..b0694c902085a 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/service_name_filter.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/service_name_filter.ts @@ -22,9 +22,9 @@ Then(`it displays relevant client metrics`, () => { cy.get('kbnLoadingIndicator').should('not.be.visible'); cy.get('.euiStat__title-isLoading').should('not.be.visible'); - cy.get(clientMetrics).eq(2).invoke('text').snapshot(); + cy.get(clientMetrics).eq(2).should('have.text', '7 '); - cy.get(clientMetrics).eq(1).invoke('text').snapshot(); + cy.get(clientMetrics).eq(1).should('have.text', '0.07 sec'); - cy.get(clientMetrics).eq(0).invoke('text').snapshot(); + cy.get(clientMetrics).eq(0).should('have.text', '0.01 sec'); }); diff --git a/x-pack/plugins/apm/public/components/app/Home/index.tsx b/x-pack/plugins/apm/public/components/app/Home/index.tsx index bcc834fef6a6a..b09c03f853aa9 100644 --- a/x-pack/plugins/apm/public/components/app/Home/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Home/index.tsx @@ -26,8 +26,6 @@ import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink' import { ServiceMap } from '../ServiceMap'; import { ServiceOverview } from '../ServiceOverview'; import { TraceOverview } from '../TraceOverview'; -import { RumOverview } from '../RumDashboard'; -import { RumOverviewLink } from '../../shared/Links/apm/RumOverviewLink'; function getHomeTabs({ serviceMapEnabled = true, @@ -73,18 +71,6 @@ function getHomeTabs({ }); } - homeTabs.push({ - link: ( - - {i18n.translate('xpack.apm.home.rumTabLabel', { - defaultMessage: 'Real User Monitoring', - })} - - ), - render: () => , - name: 'rum-overview', - }); - return homeTabs; } @@ -93,7 +79,7 @@ const SETTINGS_LINK_LABEL = i18n.translate('xpack.apm.settingsLinkLabel', { }); interface Props { - tab: 'traces' | 'services' | 'service-map' | 'rum-overview'; + tab: 'traces' | 'services' | 'service-map'; } export function Home({ tab }: Props) { diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx index 8379def2a7d9a..057971b1ca3a4 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx @@ -28,6 +28,7 @@ import { EditAgentConfigurationRouteHandler, CreateAgentConfigurationRouteHandler, } from './route_handlers/agent_configuration'; +import { RumHome } from '../../RumDashboard/RumHome'; const metricsBreadcrumb = i18n.translate('xpack.apm.breadcrumb.metricsTitle', { defaultMessage: 'Metrics', @@ -253,17 +254,8 @@ export const routes: BreadcrumbRoute[] = [ }, { exact: true, - path: '/rum-overview', - component: () => , - breadcrumb: i18n.translate('xpack.apm.home.rumOverview.title', { - defaultMessage: 'Real User Monitoring', - }), - name: RouteName.RUM_OVERVIEW, - }, - { - exact: true, - path: '/services/:serviceName/rum-overview', - component: () => , + path: '/rum-preview', + component: () => , breadcrumb: i18n.translate('xpack.apm.home.rumOverview.title', { defaultMessage: 'Real User Monitoring', }), diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownGroup.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownGroup.tsx index 007cdab0d2078..5bf84b6c918c5 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownGroup.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownGroup.tsx @@ -88,6 +88,7 @@ export const BreakdownGroup = ({ data-cy={`filter-breakdown-item_${name}`} key={name + count} onClick={onFilterItemClick(name)} + disabled={!selected && getSelItems().length > 0} > {name} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx new file mode 100644 index 0000000000000..1e28fde4aa2b4 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { + Chart, + DARK_THEME, + Datum, + LIGHT_THEME, + Partition, + PartitionLayout, + Settings, +} from '@elastic/charts'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { + EUI_CHARTS_THEME_DARK, + EUI_CHARTS_THEME_LIGHT, +} from '@elastic/eui/dist/eui_charts_theme'; +import { useUiSetting$ } from '../../../../../../../../src/plugins/kibana_react/public'; +import { ChartWrapper } from '../ChartWrapper'; + +interface Props { + options?: Array<{ + count: number; + name: string; + }>; +} + +export const VisitorBreakdownChart = ({ options }: Props) => { + const [darkMode] = useUiSetting$('theme:darkMode'); + + return ( + + + + d.count as number} + valueGetter="percent" + percentFormatter={(d: number) => + `${Math.round((d + Number.EPSILON) * 100) / 100}%` + } + layers={[ + { + groupByRollup: (d: Datum) => d.name, + nodeLabel: (d: Datum) => d, + // fillLabel: { textInvertible: true }, + shape: { + fillColor: (d) => { + const clrs = [ + euiLightVars.euiColorVis1_behindText, + euiLightVars.euiColorVis0_behindText, + euiLightVars.euiColorVis2_behindText, + euiLightVars.euiColorVis3_behindText, + euiLightVars.euiColorVis4_behindText, + euiLightVars.euiColorVis5_behindText, + euiLightVars.euiColorVis6_behindText, + euiLightVars.euiColorVis7_behindText, + euiLightVars.euiColorVis8_behindText, + euiLightVars.euiColorVis9_behindText, + ]; + return clrs[d.sortIndex]; + }, + }, + }, + ]} + config={{ + partitionLayout: PartitionLayout.sunburst, + linkLabel: { + maxCount: 32, + fontSize: 14, + }, + fontFamily: 'Arial', + margin: { top: 0, bottom: 0, left: 0, right: 0 }, + minFontSize: 1, + idealFontSizeJump: 1.1, + outerSizeRatio: 0.9, // - 0.5 * Math.random(), + emptySizeRatio: 0, + circlePadding: 4, + }} + /> + + + ); +}; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx index df72fa604e4b3..5fee2f4195f91 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx @@ -34,6 +34,7 @@ export function ClientMetrics() { }, }); } + return Promise.resolve(null); }, [start, end, serviceName, uiFilters] ); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx index 81503e16f7bcf..adeff2b31fd93 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx @@ -56,6 +56,7 @@ export const PageLoadDistribution = () => { }, }); } + return Promise.resolve(null); }, [ end, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx index 328b873ef8562..c6ef319f8a666 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx @@ -39,6 +39,7 @@ export const PageViewsTrend = () => { }, }); } + return Promise.resolve(undefined); }, [end, start, serviceName, uiFilters, breakdowns] ); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx index 326d4a00fd31f..2eb79257334d7 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx @@ -16,8 +16,9 @@ import { ClientMetrics } from './ClientMetrics'; import { PageViewsTrend } from './PageViewsTrend'; import { PageLoadDistribution } from './PageLoadDistribution'; import { I18LABELS } from './translations'; +import { VisitorBreakdown } from './VisitorBreakdown'; -export function RumDashboard() { +export const RumDashboard = () => { return ( @@ -42,7 +43,15 @@ export function RumDashboard() { + + + + + + + + ); -} +}; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHeader/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHeader/index.tsx new file mode 100644 index 0000000000000..b1ff38fdd2d79 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHeader/index.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import { DatePicker } from '../../../shared/DatePicker'; + +export const RumHeader: React.FC = ({ children }) => ( + <> + + {children} + + + + + +); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx new file mode 100644 index 0000000000000..a1b07640b5c17 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import React from 'react'; +import { RumOverview } from '../RumDashboard'; +import { RumHeader } from './RumHeader'; + +export function RumHome() { + return ( +
+ + + + +

End User Experience

+
+
+
+
+ +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx new file mode 100644 index 0000000000000..2e17e27587b63 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import { VisitorBreakdownChart } from '../Charts/VisitorBreakdownChart'; +import { VisitorBreakdownLabel } from '../translations'; +import { useFetcher } from '../../../../hooks/useFetcher'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; + +export const VisitorBreakdown = () => { + const { urlParams, uiFilters } = useUrlParams(); + + const { start, end, serviceName } = urlParams; + + const { data } = useFetcher( + (callApmApi) => { + if (start && end && serviceName) { + return callApmApi({ + pathname: '/api/apm/rum-client/visitor-breakdown', + params: { + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + }, + }, + }); + } + return Promise.resolve(null); + }, + [end, start, serviceName, uiFilters] + ); + + return ( + <> + +

{VisitorBreakdownLabel}

+
+ + + + +

Browser

+
+
+ + + +

Operating System

+
+
+ + + +

Device

+
+
+
+ + ); +}; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx index 3380a81c7bfab..9b88202b2e5ef 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiSpacer, } from '@elastic/eui'; -import React, { useMemo } from 'react'; -import { useRouteMatch } from 'react-router-dom'; import { useTrackPageview } from '../../../../../observability/public'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { PROJECTION } from '../../../../common/projections/typings'; @@ -20,6 +19,7 @@ import { ServiceNameFilter } from '../../shared/LocalUIFilters/ServiceNameFilter import { useUrlParams } from '../../../hooks/useUrlParams'; import { useFetcher } from '../../../hooks/useFetcher'; import { RUM_AGENTS } from '../../../../common/agent_name'; +import { EnvironmentFilter } from '../../shared/EnvironmentFilter'; export function RumOverview() { useTrackPageview({ app: 'apm', path: 'rum_overview' }); @@ -38,11 +38,7 @@ export function RumOverview() { urlParams: { start, end }, } = useUrlParams(); - const isRumServiceRoute = useRouteMatch( - '/services/:serviceName/rum-overview' - ); - - const { data } = useFetcher( + const { data, status } = useFetcher( (callApmApi) => { if (start && end) { return callApmApi({ @@ -65,14 +61,17 @@ export function RumOverview() { + + - {!isRumServiceRoute && ( - <> - - - {' '} - - )} + <> + + + {' '} + diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts index 2784d9bfd8efa..96d1b529c52f9 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts @@ -50,3 +50,10 @@ export const I18LABELS = { defaultMessage: 'seconds', }), }; + +export const VisitorBreakdownLabel = i18n.translate( + 'xpack.apm.rum.visitorBreakdown', + { + defaultMessage: 'Visitor breakdown', + } +); diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx index ce60ffa4ba4e3..2f35e329720de 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx @@ -22,17 +22,9 @@ import { ServiceMap } from '../ServiceMap'; import { ServiceMetrics } from '../ServiceMetrics'; import { ServiceNodeOverview } from '../ServiceNodeOverview'; import { TransactionOverview } from '../TransactionOverview'; -import { RumOverview } from '../RumDashboard'; -import { RumOverviewLink } from '../../shared/Links/apm/RumOverviewLink'; interface Props { - tab: - | 'transactions' - | 'errors' - | 'metrics' - | 'nodes' - | 'service-map' - | 'rum-overview'; + tab: 'transactions' | 'errors' | 'metrics' | 'nodes' | 'service-map'; } export function ServiceDetailTabs({ tab }: Props) { @@ -118,20 +110,6 @@ export function ServiceDetailTabs({ tab }: Props) { tabs.push(serviceMapTab); } - if (isRumAgentName(agentName)) { - tabs.push({ - link: ( - - {i18n.translate('xpack.apm.home.rumTabLabel', { - defaultMessage: 'Real User Monitoring', - })} - - ), - render: () => , - name: 'rum-overview', - }); - } - const selectedTab = tabs.find((serviceTab) => serviceTab.name === tab); return ( diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx index eab685a4c1ab4..6ddc4eecba7ed 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -76,7 +76,7 @@ export function KueryBar() { }); // The bar should be disabled when viewing the service map - const disabled = /\/(service-map|rum-overview)$/.test(location.pathname); + const disabled = /\/(service-map)$/.test(location.pathname); const disabledPlaceholder = i18n.translate( 'xpack.apm.kueryBar.disabledPlaceholder', { defaultMessage: 'Search is not available here' } diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/RumOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/RumOverviewLink.tsx deleted file mode 100644 index 729ed9b10f827..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/RumOverviewLink.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { APMLink, APMLinkExtendProps } from './APMLink'; - -interface RumOverviewLinkProps extends APMLinkExtendProps { - serviceName?: string; -} -export function RumOverviewLink({ - serviceName, - ...rest -}: RumOverviewLinkProps) { - const path = serviceName - ? `/services/${serviceName}/rum-overview` - : '/rum-overview'; - - return ; -} diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx index 0bb62bd8efcff..405a4cacae714 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx @@ -18,9 +18,10 @@ import { fromQuery, toQuery } from '../../Links/url_helpers'; interface Props { serviceNames: string[]; + loading: boolean; } -const ServiceNameFilter = ({ serviceNames }: Props) => { +const ServiceNameFilter = ({ loading, serviceNames }: Props) => { const { urlParams: { serviceName }, } = useUrlParams(); @@ -60,6 +61,7 @@ const ServiceNameFilter = ({ serviceNames }: Props) => { ({ + count: bucket.doc_count, + name: bucket.key as string, + })), + os: os.buckets.map((bucket) => ({ + count: bucket.doc_count, + name: bucket.key as string, + })), + devices: devices.buckets.map((bucket) => ({ + count: bucket.doc_count, + name: bucket.key as string, + })), + }; +} diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 4e3aa6d4ebe1d..11911cda79c17 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -77,6 +77,7 @@ import { rumPageLoadDistributionRoute, rumPageLoadDistBreakdownRoute, rumServicesRoute, + rumVisitorsBreakdownRoute, } from './rum_client'; import { observabilityOverviewHasDataRoute, @@ -174,6 +175,7 @@ const createApmApi = () => { .add(rumPageLoadDistBreakdownRoute) .add(rumClientMetricsRoute) .add(rumServicesRoute) + .add(rumVisitorsBreakdownRoute) // Observability dashboard .add(observabilityOverviewHasDataRoute) diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts index 01e549632a0bc..0781512c6f7a0 100644 --- a/x-pack/plugins/apm/server/routes/rum_client.ts +++ b/x-pack/plugins/apm/server/routes/rum_client.ts @@ -13,6 +13,7 @@ import { getPageViewTrends } from '../lib/rum_client/get_page_view_trends'; import { getPageLoadDistribution } from '../lib/rum_client/get_page_load_distribution'; import { getPageLoadDistBreakdown } from '../lib/rum_client/get_pl_dist_breakdown'; import { getRumServices } from '../lib/rum_client/get_rum_services'; +import { getVisitorBreakdown } from '../lib/rum_client/get_visitor_breakdown'; export const percentileRangeRt = t.partial({ minPercentile: t.string, @@ -104,3 +105,15 @@ export const rumServicesRoute = createRoute(() => ({ return getRumServices({ setup }); }, })); + +export const rumVisitorsBreakdownRoute = createRoute(() => ({ + path: '/api/apm/rum-client/visitor-breakdown', + params: { + query: t.intersection([uiFiltersRt, rangeRt]), + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + return getVisitorBreakdown({ setup }); + }, +})); From cdbe12ff577292a7c69562c4e2c1d38c9b35308f Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 14 Jul 2020 22:41:58 +0200 Subject: [PATCH 35/82] [Lens] XY chart -long legend overflows chart in editor Feature:Lens (#70702) --- .../_workspace_panel_wrapper.scss | 4 ++ .../workspace_panel_wrapper.tsx | 44 +++++++++---------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss index e663754707e05..90cc049db96eb 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss @@ -36,3 +36,7 @@ } } } + +.lnsWorkspacePanelWrapper__toolbar { + margin-bottom: $euiSizeS; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx index f21939b3a2895..f6e15002ca66c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx @@ -66,8 +66,8 @@ export function WorkspacePanelWrapper({ [dispatch] ); return ( - - + <> +
)} - - - - {(!emptyExpression || title) && ( - - - {title || - i18n.translate('xpack.lens.chartTitle.unsaved', { defaultMessage: 'Unsaved' })} - - - )} - - {children} - - - - +
+ + {(!emptyExpression || title) && ( + + + {title || + i18n.translate('xpack.lens.chartTitle.unsaved', { defaultMessage: 'Unsaved' })} + + + )} + + {children} + + + ); } From 820f9ede2dcf649114305988f989ced2805cc7ad Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 14 Jul 2020 13:47:38 -0700 Subject: [PATCH 36/82] [Reporting] Move a few server files for shorter paths (#71591) --- src/dev/precommit_hook/casing_check_config.js | 12 ++++++------ x-pack/plugins/reporting/common/types.ts | 2 +- .../chromium/driver/chromium_driver.ts | 2 +- x-pack/plugins/reporting/server/core.ts | 2 +- .../server/export_types/common/constants.ts | 7 ------- .../decrypt_job_headers.test.ts | 4 ++-- .../{execute_job => }/decrypt_job_headers.ts | 2 +- .../common/get_absolute_url.test.ts | 0 .../export_types}/common/get_absolute_url.ts | 0 .../get_conditional_headers.test.ts | 12 ++++++------ .../get_conditional_headers.ts | 4 ++-- .../{execute_job => }/get_custom_logo.test.ts | 8 ++++---- .../{execute_job => }/get_custom_logo.ts | 8 ++++---- .../{execute_job => }/get_full_urls.test.ts | 6 +++--- .../common/{execute_job => }/get_full_urls.ts | 10 +++++----- .../common/{execute_job => }/index.ts | 1 + .../omit_blacklisted_headers.test.ts | 0 .../omit_blacklisted_headers.ts | 2 +- .../common/validate_urls.test.ts | 0 .../export_types}/common/validate_urls.ts | 0 .../csv/{server => }/create_job.ts | 6 +++--- .../csv/{server => }/execute_job.test.ts | 18 +++++++++--------- .../csv/{server => }/execute_job.ts | 10 +++++----- .../generate_csv/cell_has_formula.ts | 2 +- .../check_cells_for_formulas.test.ts | 0 .../generate_csv/check_cells_for_formulas.ts | 0 .../generate_csv/escape_value.test.ts | 0 .../{server => }/generate_csv/escape_value.ts | 2 +- .../generate_csv/field_format_map.test.ts | 2 +- .../generate_csv/field_format_map.ts | 2 +- .../generate_csv/flatten_hit.test.ts | 0 .../{server => }/generate_csv/flatten_hit.ts | 0 .../generate_csv/format_csv_values.test.ts | 0 .../generate_csv/format_csv_values.ts | 2 +- .../generate_csv/get_ui_settings.ts | 4 ++-- .../generate_csv/hit_iterator.test.ts | 6 +++--- .../{server => }/generate_csv/hit_iterator.ts | 6 +++--- .../csv/{server => }/generate_csv/index.ts | 12 ++++++------ .../max_size_string_builder.test.ts | 0 .../generate_csv/max_size_string_builder.ts | 0 .../server/export_types/csv/index.ts | 4 ++-- .../csv/{server => }/lib/get_request.ts | 4 ++-- .../{server => }/create_job.ts | 8 ++++---- .../{server => }/execute_job.ts | 10 +++++----- .../csv_from_savedobject/index.ts | 8 ++++---- .../{server => }/lib/get_csv_job.test.ts | 2 +- .../{server => }/lib/get_csv_job.ts | 6 +++--- .../{server => }/lib/get_data_source.ts | 4 ++-- .../{server => }/lib/get_fake_request.ts | 6 +++--- .../{server => }/lib/get_filters.test.ts | 4 ++-- .../{server => }/lib/get_filters.ts | 4 ++-- .../png/{server => }/create_job/index.ts | 8 ++++---- .../{server => }/execute_job/index.test.ts | 10 +++++----- .../png/{server => }/execute_job/index.ts | 8 ++++---- .../server/export_types/png/index.ts | 6 +++--- .../png/{server => }/lib/generate_png.ts | 9 ++++----- .../server/export_types/png/types.d.ts | 2 +- .../{server => }/create_job/index.ts | 8 ++++---- .../{server => }/execute_job/index.test.ts | 10 +++++----- .../{server => }/execute_job/index.ts | 8 ++++---- .../export_types/printable_pdf/index.ts | 4 ++-- .../{server => }/lib/generate_pdf.ts | 8 ++++---- .../lib/pdf/assets/fonts/noto/LICENSE_OFL.txt | 0 .../fonts/noto/NotoSansCJKtc-Medium.ttf | Bin .../fonts/noto/NotoSansCJKtc-Regular.ttf | Bin .../lib/pdf/assets/fonts/noto/index.js | 0 .../lib/pdf/assets/fonts/roboto/LICENSE.txt | 0 .../pdf/assets/fonts/roboto/Roboto-Italic.ttf | Bin .../pdf/assets/fonts/roboto/Roboto-Medium.ttf | Bin .../assets/fonts/roboto/Roboto-Regular.ttf | Bin .../lib/pdf/assets/img/logo-grey.png | Bin .../{server => }/lib/pdf/index.js | 0 .../printable_pdf/{server => }/lib/tracker.ts | 0 .../{server => }/lib/uri_encode.js | 2 +- .../export_types/printable_pdf/types.d.ts | 2 +- .../reporting/server/lib/create_queue.ts | 2 +- .../lib/{ => esqueue}/create_tagged_logger.ts | 2 +- x-pack/plugins/reporting/server/lib/index.ts | 6 +++--- .../common => lib}/layouts/create_layout.ts | 2 +- .../common => lib}/layouts/index.ts | 4 ++-- .../common => lib}/layouts/layout.ts | 0 .../layouts/preserve_layout.css | 0 .../common => lib}/layouts/preserve_layout.ts | 0 .../common => lib}/layouts/print.css | 2 +- .../common => lib}/layouts/print_layout.ts | 8 ++++---- .../common => }/lib/screenshots/constants.ts | 2 ++ .../screenshots/get_element_position_data.ts | 8 ++++---- .../lib/screenshots/get_number_of_items.ts | 9 ++++----- .../lib/screenshots/get_screenshots.ts | 6 +++--- .../lib/screenshots/get_time_range.ts | 6 +++--- .../common => }/lib/screenshots/index.ts | 0 .../common => }/lib/screenshots/inject_css.ts | 6 +++--- .../lib/screenshots/observable.test.ts | 12 ++++++------ .../common => }/lib/screenshots/observable.ts | 6 +++--- .../common => }/lib/screenshots/open_url.ts | 6 +++--- .../lib/screenshots/wait_for_render.ts | 8 ++++---- .../screenshots/wait_for_visualizations.ts | 8 ++++---- .../reporting/server/lib/store/store.ts | 2 +- .../reporting/server/lib/validate/index.ts | 2 +- .../validate/validate_max_content_length.ts | 2 +- .../generate_from_savedobject_immediate.ts | 4 ++-- .../plugins/reporting/server/routes/jobs.ts | 2 +- .../routes/lib/authorized_user_pre_routing.ts | 2 +- .../server/{ => routes}/lib/get_user.ts | 2 +- .../server/routes/lib/job_response_handler.ts | 2 +- .../server/{ => routes}/lib/jobs_query.ts | 6 +++--- .../create_mock_browserdriverfactory.ts | 2 +- .../create_mock_layoutinstance.ts | 2 +- x-pack/plugins/reporting/server/types.ts | 2 +- 109 files changed, 213 insertions(+), 219 deletions(-) delete mode 100644 x-pack/plugins/reporting/server/export_types/common/constants.ts rename x-pack/plugins/reporting/server/export_types/common/{execute_job => }/decrypt_job_headers.test.ts (93%) rename x-pack/plugins/reporting/server/export_types/common/{execute_job => }/decrypt_job_headers.ts (96%) rename x-pack/plugins/reporting/{ => server/export_types}/common/get_absolute_url.test.ts (100%) rename x-pack/plugins/reporting/{ => server/export_types}/common/get_absolute_url.ts (100%) rename x-pack/plugins/reporting/server/export_types/common/{execute_job => }/get_conditional_headers.test.ts (93%) rename x-pack/plugins/reporting/server/export_types/common/{execute_job => }/get_conditional_headers.ts (91%) rename x-pack/plugins/reporting/server/export_types/common/{execute_job => }/get_custom_logo.test.ts (85%) rename x-pack/plugins/reporting/server/export_types/common/{execute_job => }/get_custom_logo.ts (82%) rename x-pack/plugins/reporting/server/export_types/common/{execute_job => }/get_full_urls.test.ts (97%) rename x-pack/plugins/reporting/server/export_types/common/{execute_job => }/get_full_urls.ts (90%) rename x-pack/plugins/reporting/server/export_types/common/{execute_job => }/index.ts (91%) rename x-pack/plugins/reporting/server/export_types/common/{execute_job => }/omit_blacklisted_headers.test.ts (100%) rename x-pack/plugins/reporting/server/export_types/common/{execute_job => }/omit_blacklisted_headers.ts (95%) rename x-pack/plugins/reporting/{ => server/export_types}/common/validate_urls.test.ts (100%) rename x-pack/plugins/reporting/{ => server/export_types}/common/validate_urls.ts (100%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/create_job.ts (90%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/execute_job.test.ts (98%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/execute_job.ts (92%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/cell_has_formula.ts (85%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/check_cells_for_formulas.test.ts (100%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/check_cells_for_formulas.ts (100%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/escape_value.test.ts (100%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/escape_value.ts (95%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/field_format_map.test.ts (97%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/field_format_map.ts (97%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/flatten_hit.test.ts (100%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/flatten_hit.ts (100%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/format_csv_values.test.ts (100%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/format_csv_values.ts (97%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/get_ui_settings.ts (94%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/hit_iterator.test.ts (96%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/hit_iterator.ts (95%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/index.ts (93%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/max_size_string_builder.test.ts (100%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/max_size_string_builder.ts (100%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/lib/get_request.ts (93%) rename x-pack/plugins/reporting/server/export_types/csv_from_savedobject/{server => }/create_job.ts (94%) rename x-pack/plugins/reporting/server/export_types/csv_from_savedobject/{server => }/execute_job.ts (93%) rename x-pack/plugins/reporting/server/export_types/csv_from_savedobject/{server => }/lib/get_csv_job.test.ts (99%) rename x-pack/plugins/reporting/server/export_types/csv_from_savedobject/{server => }/lib/get_csv_job.ts (96%) rename x-pack/plugins/reporting/server/export_types/csv_from_savedobject/{server => }/lib/get_data_source.ts (95%) rename x-pack/plugins/reporting/server/export_types/csv_from_savedobject/{server => }/lib/get_fake_request.ts (90%) rename x-pack/plugins/reporting/server/export_types/csv_from_savedobject/{server => }/lib/get_filters.test.ts (98%) rename x-pack/plugins/reporting/server/export_types/csv_from_savedobject/{server => }/lib/get_filters.ts (95%) rename x-pack/plugins/reporting/server/export_types/png/{server => }/create_job/index.ts (85%) rename x-pack/plugins/reporting/server/export_types/png/{server => }/execute_job/index.test.ts (94%) rename x-pack/plugins/reporting/server/export_types/png/{server => }/execute_job/index.ts (93%) rename x-pack/plugins/reporting/server/export_types/png/{server => }/lib/generate_png.ts (89%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/create_job/index.ts (86%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/execute_job/index.test.ts (93%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/execute_job/index.ts (94%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/generate_pdf.ts (96%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/pdf/assets/fonts/noto/LICENSE_OFL.txt (100%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf (100%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf (100%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/pdf/assets/fonts/noto/index.js (100%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/pdf/assets/fonts/roboto/LICENSE.txt (100%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf (100%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf (100%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf (100%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/pdf/assets/img/logo-grey.png (100%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/pdf/index.js (100%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/tracker.ts (100%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/uri_encode.js (92%) rename x-pack/plugins/reporting/server/lib/{ => esqueue}/create_tagged_logger.ts (95%) rename x-pack/plugins/reporting/server/{export_types/common => lib}/layouts/create_layout.ts (94%) rename x-pack/plugins/reporting/server/{export_types/common => lib}/layouts/index.ts (94%) rename x-pack/plugins/reporting/server/{export_types/common => lib}/layouts/layout.ts (100%) rename x-pack/plugins/reporting/server/{export_types/common => lib}/layouts/preserve_layout.css (100%) rename x-pack/plugins/reporting/server/{export_types/common => lib}/layouts/preserve_layout.ts (100%) rename x-pack/plugins/reporting/server/{export_types/common => lib}/layouts/print.css (96%) rename x-pack/plugins/reporting/server/{export_types/common => lib}/layouts/print_layout.ts (91%) rename x-pack/plugins/reporting/server/{export_types/common => }/lib/screenshots/constants.ts (92%) rename x-pack/plugins/reporting/server/{export_types/common => }/lib/screenshots/get_element_position_data.ts (93%) rename x-pack/plugins/reporting/server/{export_types/common => }/lib/screenshots/get_number_of_items.ts (91%) rename x-pack/plugins/reporting/server/{export_types/common => }/lib/screenshots/get_screenshots.ts (91%) rename x-pack/plugins/reporting/server/{export_types/common => }/lib/screenshots/get_time_range.ts (87%) rename x-pack/plugins/reporting/server/{export_types/common => }/lib/screenshots/index.ts (100%) rename x-pack/plugins/reporting/server/{export_types/common => }/lib/screenshots/inject_css.ts (90%) rename x-pack/plugins/reporting/server/{export_types/common => }/lib/screenshots/observable.test.ts (97%) rename x-pack/plugins/reporting/server/{export_types/common => }/lib/screenshots/observable.ts (97%) rename x-pack/plugins/reporting/server/{export_types/common => }/lib/screenshots/open_url.ts (85%) rename x-pack/plugins/reporting/server/{export_types/common => }/lib/screenshots/wait_for_render.ts (92%) rename x-pack/plugins/reporting/server/{export_types/common => }/lib/screenshots/wait_for_visualizations.ts (90%) rename x-pack/plugins/reporting/server/{ => routes}/lib/get_user.ts (87%) rename x-pack/plugins/reporting/server/{ => routes}/lib/jobs_query.ts (96%) diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index cec80dd547a53..b8eacdd6a3897 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -173,12 +173,12 @@ export const TEMPORARILY_IGNORED_PATHS = [ 'x-pack/plugins/monitoring/public/icons/health-green.svg', 'x-pack/plugins/monitoring/public/icons/health-red.svg', 'x-pack/plugins/monitoring/public/icons/health-yellow.svg', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/img/logo-grey.png', + 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf', + 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf', + 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf', + 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf', + 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf', + 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/img/logo-grey.png', 'x-pack/test/functional/es_archives/monitoring/beats-with-restarted-instance/data.json.gz', 'x-pack/test/functional/es_archives/monitoring/beats-with-restarted-instance/mappings.json', 'x-pack/test/functional/es_archives/monitoring/logstash-pipelines/data.json.gz', diff --git a/x-pack/plugins/reporting/common/types.ts b/x-pack/plugins/reporting/common/types.ts index 2819c28cfb54f..18b0ac2a72802 100644 --- a/x-pack/plugins/reporting/common/types.ts +++ b/x-pack/plugins/reporting/common/types.ts @@ -7,7 +7,7 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths export { ReportingConfigType } from '../server/config'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { LayoutInstance } from '../server/export_types/common/layouts'; +export { LayoutInstance } from '../server/lib/layouts'; export type JobId = string; export type JobStatus = diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts index bca9496bc9add..eb16a9d6de1a8 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts @@ -9,8 +9,8 @@ import { map, truncate } from 'lodash'; import open from 'opn'; import { ElementHandle, EvaluateFn, Page, Response, SerializableOrJSHandle } from 'puppeteer'; import { parse as parseUrl } from 'url'; -import { ViewZoomWidthHeight } from '../../../export_types/common/layouts/layout'; import { LevelLogger } from '../../../lib'; +import { ViewZoomWidthHeight } from '../../../lib/layouts/layout'; import { ConditionalHeaders, ElementPosition } from '../../../types'; import { allowRequest, NetworkPolicy } from '../../network_policy'; diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index eccd6c7db1698..95dc7586ad4a6 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -20,7 +20,7 @@ import { SecurityPluginSetup } from '../../security/server'; import { ScreenshotsObservableFn } from '../server/types'; import { ReportingConfig } from './'; import { HeadlessChromiumDriverFactory } from './browsers/chromium/driver_factory'; -import { screenshotsObservableFactory } from './export_types/common/lib/screenshots'; +import { screenshotsObservableFactory } from './lib/screenshots'; import { checkLicense, getExportTypesRegistry } from './lib'; import { ESQueueInstance } from './lib/create_queue'; import { EnqueueJobFn } from './lib/enqueue_job'; diff --git a/x-pack/plugins/reporting/server/export_types/common/constants.ts b/x-pack/plugins/reporting/server/export_types/common/constants.ts deleted file mode 100644 index 76fab923978f8..0000000000000 --- a/x-pack/plugins/reporting/server/export_types/common/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const DEFAULT_PAGELOAD_SELECTOR = '.application'; diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/decrypt_job_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts similarity index 93% rename from x-pack/plugins/reporting/server/export_types/common/execute_job/decrypt_job_headers.test.ts rename to x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts index 4998d936c9b16..908817a2ccf81 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/decrypt_job_headers.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { cryptoFactory, LevelLogger } from '../../../lib'; -import { decryptJobHeaders } from './decrypt_job_headers'; +import { cryptoFactory, LevelLogger } from '../../lib'; +import { decryptJobHeaders } from './'; const encryptHeaders = async (encryptionKey: string, headers: Record) => { const crypto = cryptoFactory(encryptionKey); diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/decrypt_job_headers.ts b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts similarity index 96% rename from x-pack/plugins/reporting/server/export_types/common/execute_job/decrypt_job_headers.ts rename to x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts index 579b5196ad4d9..845b9adb38be9 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/decrypt_job_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { cryptoFactory, LevelLogger } from '../../../lib'; +import { cryptoFactory, LevelLogger } from '../../lib'; interface HasEncryptedHeaders { headers?: string; diff --git a/x-pack/plugins/reporting/common/get_absolute_url.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_absolute_url.test.ts similarity index 100% rename from x-pack/plugins/reporting/common/get_absolute_url.test.ts rename to x-pack/plugins/reporting/server/export_types/common/get_absolute_url.test.ts diff --git a/x-pack/plugins/reporting/common/get_absolute_url.ts b/x-pack/plugins/reporting/server/export_types/common/get_absolute_url.ts similarity index 100% rename from x-pack/plugins/reporting/common/get_absolute_url.ts rename to x-pack/plugins/reporting/server/export_types/common/get_absolute_url.ts diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts similarity index 93% rename from x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.test.ts rename to x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts index 030ced5dc4b80..0372d515c21a8 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts @@ -5,12 +5,12 @@ */ import sinon from 'sinon'; -import { ReportingConfig } from '../../../'; -import { ReportingCore } from '../../../core'; -import { createMockReportingCore } from '../../../test_helpers'; -import { ScheduledTaskParams } from '../../../types'; -import { ScheduledTaskParamsPDF } from '../../printable_pdf/types'; -import { getConditionalHeaders, getCustomLogo } from './index'; +import { ReportingConfig } from '../../'; +import { ReportingCore } from '../../core'; +import { createMockReportingCore } from '../../test_helpers'; +import { ScheduledTaskParams } from '../../types'; +import { ScheduledTaskParamsPDF } from '../printable_pdf/types'; +import { getConditionalHeaders, getCustomLogo } from './'; let mockConfig: ReportingConfig; let mockReportingPlugin: ReportingCore; diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.ts b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.ts similarity index 91% rename from x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.ts rename to x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.ts index 7a50eaac80d85..799d023486832 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReportingConfig } from '../../../'; -import { ConditionalHeaders } from '../../../types'; +import { ReportingConfig } from '../../'; +import { ConditionalHeaders } from '../../types'; export const getConditionalHeaders = ({ config, diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts similarity index 85% rename from x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.test.ts rename to x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts index c364752c8dd0f..a3d65a1398a20 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReportingCore } from '../../../core'; -import { createMockReportingCore } from '../../../test_helpers'; -import { ScheduledTaskParamsPDF } from '../../printable_pdf/types'; -import { getConditionalHeaders, getCustomLogo } from './index'; +import { ReportingCore } from '../../core'; +import { createMockReportingCore } from '../../test_helpers'; +import { ScheduledTaskParamsPDF } from '../printable_pdf/types'; +import { getConditionalHeaders, getCustomLogo } from './'; const mockConfigGet = jest.fn().mockImplementation((key: string) => { return 'localhost'; diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.ts b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts similarity index 82% rename from x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.ts rename to x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts index 36c02eb47565c..547cc45258dae 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReportingConfig, ReportingCore } from '../../../'; -import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../../common/constants'; -import { ConditionalHeaders } from '../../../types'; -import { ScheduledTaskParamsPDF } from '../../printable_pdf/types'; // Logo is PDF only +import { ReportingConfig, ReportingCore } from '../../'; +import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../common/constants'; +import { ConditionalHeaders } from '../../types'; +import { ScheduledTaskParamsPDF } from '../printable_pdf/types'; // Logo is PDF only export const getCustomLogo = async ({ reporting, diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts similarity index 97% rename from x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.test.ts rename to x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts index ad952c084d4f3..73d7c7b03c128 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReportingConfig } from '../../../'; -import { ScheduledTaskParamsPNG } from '../../png/types'; -import { ScheduledTaskParamsPDF } from '../../printable_pdf/types'; +import { ReportingConfig } from '../../'; +import { ScheduledTaskParamsPNG } from '../png/types'; +import { ScheduledTaskParamsPDF } from '../printable_pdf/types'; import { getFullUrls } from './get_full_urls'; interface FullUrlsOpts { diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.ts b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts similarity index 90% rename from x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.ts rename to x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts index 67bc8d16fa758..d3362fd190680 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts @@ -10,11 +10,11 @@ import { UrlWithParsedQuery, UrlWithStringQuery, } from 'url'; -import { ReportingConfig } from '../../..'; -import { getAbsoluteUrlFactory } from '../../../../common/get_absolute_url'; -import { validateUrls } from '../../../../common/validate_urls'; -import { ScheduledTaskParamsPNG } from '../../png/types'; -import { ScheduledTaskParamsPDF } from '../../printable_pdf/types'; +import { ReportingConfig } from '../../'; +import { ScheduledTaskParamsPNG } from '../png/types'; +import { ScheduledTaskParamsPDF } from '../printable_pdf/types'; +import { getAbsoluteUrlFactory } from './get_absolute_url'; +import { validateUrls } from './validate_urls'; function isPngJob( job: ScheduledTaskParamsPNG | ScheduledTaskParamsPDF diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/common/index.ts similarity index 91% rename from x-pack/plugins/reporting/server/export_types/common/execute_job/index.ts rename to x-pack/plugins/reporting/server/export_types/common/index.ts index b9d59b2be1296..a4e114d6b2f2e 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/common/index.ts @@ -9,3 +9,4 @@ export { getConditionalHeaders } from './get_conditional_headers'; export { getCustomLogo } from './get_custom_logo'; export { getFullUrls } from './get_full_urls'; export { omitBlacklistedHeaders } from './omit_blacklisted_headers'; +export { validateUrls } from './validate_urls'; diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/omit_blacklisted_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/omit_blacklisted_headers.test.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/common/execute_job/omit_blacklisted_headers.test.ts rename to x-pack/plugins/reporting/server/export_types/common/omit_blacklisted_headers.test.ts diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/omit_blacklisted_headers.ts b/x-pack/plugins/reporting/server/export_types/common/omit_blacklisted_headers.ts similarity index 95% rename from x-pack/plugins/reporting/server/export_types/common/execute_job/omit_blacklisted_headers.ts rename to x-pack/plugins/reporting/server/export_types/common/omit_blacklisted_headers.ts index 305fb6bab5478..e56ffc737764c 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/omit_blacklisted_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/omit_blacklisted_headers.ts @@ -7,7 +7,7 @@ import { omitBy } from 'lodash'; import { KBN_SCREENSHOT_HEADER_BLACKLIST, KBN_SCREENSHOT_HEADER_BLACKLIST_STARTS_WITH_PATTERN, -} from '../../../../common/constants'; +} from '../../../common/constants'; export const omitBlacklistedHeaders = ({ job, diff --git a/x-pack/plugins/reporting/common/validate_urls.test.ts b/x-pack/plugins/reporting/server/export_types/common/validate_urls.test.ts similarity index 100% rename from x-pack/plugins/reporting/common/validate_urls.test.ts rename to x-pack/plugins/reporting/server/export_types/common/validate_urls.test.ts diff --git a/x-pack/plugins/reporting/common/validate_urls.ts b/x-pack/plugins/reporting/server/export_types/common/validate_urls.ts similarity index 100% rename from x-pack/plugins/reporting/common/validate_urls.ts rename to x-pack/plugins/reporting/server/export_types/common/validate_urls.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts similarity index 90% rename from x-pack/plugins/reporting/server/export_types/csv/server/create_job.ts rename to x-pack/plugins/reporting/server/export_types/csv/create_job.ts index fb2d9bfdc5838..5e8ce923a79e0 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { cryptoFactory } from '../../../lib'; -import { ESQueueCreateJobFn, ScheduleTaskFnFactory } from '../../../types'; -import { JobParamsDiscoverCsv } from '../types'; +import { cryptoFactory } from '../../lib'; +import { ESQueueCreateJobFn, ScheduleTaskFnFactory } from '../../types'; +import { JobParamsDiscoverCsv } from './types'; export const scheduleTaskFnFactory: ScheduleTaskFnFactory new Promise((resolve) => setTimeout(() => resolve(), ms)); diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts similarity index 92% rename from x-pack/plugins/reporting/server/export_types/csv/server/execute_job.ts rename to x-pack/plugins/reporting/server/export_types/csv/execute_job.ts index b38cd8c5af9e7..f0c41a6a49703 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts @@ -7,11 +7,11 @@ import { Crypto } from '@elastic/node-crypto'; import { i18n } from '@kbn/i18n'; import Hapi from 'hapi'; -import { KibanaRequest } from '../../../../../../../src/core/server'; -import { CONTENT_TYPE_CSV, CSV_JOB_TYPE } from '../../../../common/constants'; -import { cryptoFactory, LevelLogger } from '../../../lib'; -import { ESQueueWorkerExecuteFn, RunTaskFnFactory } from '../../../types'; -import { ScheduledTaskParamsCSV } from '../types'; +import { KibanaRequest } from '../../../../../../src/core/server'; +import { CONTENT_TYPE_CSV, CSV_JOB_TYPE } from '../../../common/constants'; +import { cryptoFactory, LevelLogger } from '../../lib'; +import { ESQueueWorkerExecuteFn, RunTaskFnFactory } from '../../types'; +import { ScheduledTaskParamsCSV } from './types'; import { createGenerateCsv } from './generate_csv'; const getRequest = async (headers: string | undefined, crypto: Crypto, logger: LevelLogger) => { diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/cell_has_formula.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/cell_has_formula.ts similarity index 85% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/cell_has_formula.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/cell_has_formula.ts index 659aef85ed593..1433d852ce630 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/cell_has_formula.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/cell_has_formula.ts @@ -5,7 +5,7 @@ */ import { startsWith } from 'lodash'; -import { CSV_FORMULA_CHARS } from '../../../../../common/constants'; +import { CSV_FORMULA_CHARS } from '../../../../common/constants'; export const cellHasFormulas = (val: string) => CSV_FORMULA_CHARS.some((formulaChar) => startsWith(val, formulaChar)); diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/check_cells_for_formulas.test.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/check_cells_for_formulas.test.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/check_cells_for_formulas.test.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/check_cells_for_formulas.test.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/check_cells_for_formulas.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/check_cells_for_formulas.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/check_cells_for_formulas.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/check_cells_for_formulas.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/escape_value.test.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/escape_value.test.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/escape_value.test.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/escape_value.test.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/escape_value.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/escape_value.ts similarity index 95% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/escape_value.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/escape_value.ts index 344091ee18268..c850d8b2dc741 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/escape_value.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/escape_value.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RawValue } from '../../types'; +import { RawValue } from '../types'; import { cellHasFormulas } from './cell_has_formula'; const nonAlphaNumRE = /[^a-zA-Z0-9]/; diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/field_format_map.test.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/field_format_map.test.ts similarity index 97% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/field_format_map.test.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/field_format_map.test.ts index 1f0e450da698f..4cb8de5810584 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/field_format_map.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/field_format_map.test.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { fieldFormats, FieldFormatsGetConfigFn, UI_SETTINGS } from 'src/plugins/data/server'; -import { IndexPatternSavedObject } from '../../types'; +import { IndexPatternSavedObject } from '../types'; import { fieldFormatMapFactory } from './field_format_map'; type ConfigValue = { number: { id: string; params: {} } } | string; diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/field_format_map.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/field_format_map.ts similarity index 97% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/field_format_map.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/field_format_map.ts index 848cf569bc8d7..e01fee530fc65 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/field_format_map.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/field_format_map.ts @@ -7,7 +7,7 @@ import _ from 'lodash'; import { FieldFormat } from 'src/plugins/data/common'; import { FieldFormatConfig, IFieldFormatsRegistry } from 'src/plugins/data/server'; -import { IndexPatternSavedObject } from '../../types'; +import { IndexPatternSavedObject } from '../types'; /** * Create a map of FieldFormat instances for index pattern fields diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/flatten_hit.test.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/flatten_hit.test.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/flatten_hit.test.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/flatten_hit.test.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/flatten_hit.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/flatten_hit.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/flatten_hit.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/flatten_hit.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/format_csv_values.test.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/format_csv_values.test.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/format_csv_values.test.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/format_csv_values.test.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/format_csv_values.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/format_csv_values.ts similarity index 97% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/format_csv_values.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/format_csv_values.ts index 387066415a1bc..d0294072112bf 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/format_csv_values.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/format_csv_values.ts @@ -6,7 +6,7 @@ import { isNull, isObject, isUndefined } from 'lodash'; import { FieldFormat } from 'src/plugins/data/common'; -import { RawValue } from '../../types'; +import { RawValue } from '../types'; export function createFormatCsvValues( escapeValue: (value: RawValue, index: number, array: RawValue[]) => string, diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/get_ui_settings.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/get_ui_settings.ts similarity index 94% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/get_ui_settings.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/get_ui_settings.ts index 8f72c467b0711..915d5010a4885 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/get_ui_settings.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/get_ui_settings.ts @@ -6,8 +6,8 @@ import { i18n } from '@kbn/i18n'; import { IUiSettingsClient } from 'kibana/server'; -import { ReportingConfig } from '../../../..'; -import { LevelLogger } from '../../../../lib'; +import { ReportingConfig } from '../../../'; +import { LevelLogger } from '../../../lib'; export const getUiSettings = async ( timezone: string | undefined, diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/hit_iterator.test.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.test.ts similarity index 96% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/hit_iterator.test.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.test.ts index 479879e3c8b01..831bf45cf72ea 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/hit_iterator.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.test.ts @@ -6,9 +6,9 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; -import { CancellationToken } from '../../../../../common'; -import { LevelLogger } from '../../../../lib'; -import { ScrollConfig } from '../../../../types'; +import { CancellationToken } from '../../../../common'; +import { LevelLogger } from '../../../lib'; +import { ScrollConfig } from '../../../types'; import { createHitIterator } from './hit_iterator'; const mockLogger = { diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/hit_iterator.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.ts similarity index 95% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/hit_iterator.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.ts index b877023064ac6..dee653cf30007 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/hit_iterator.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.ts @@ -6,9 +6,9 @@ import { i18n } from '@kbn/i18n'; import { SearchParams, SearchResponse } from 'elasticsearch'; -import { CancellationToken } from '../../../../../common'; -import { LevelLogger } from '../../../../lib'; -import { ScrollConfig } from '../../../../types'; +import { CancellationToken } from '../../../../common'; +import { LevelLogger } from '../../../lib'; +import { ScrollConfig } from '../../../types'; export type EndpointCaller = (method: string, params: object) => Promise>; diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/index.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts similarity index 93% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/index.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts index 2cb10e291619c..8da27100ac31c 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts @@ -6,12 +6,12 @@ import { i18n } from '@kbn/i18n'; import { IUiSettingsClient } from 'src/core/server'; -import { getFieldFormats } from '../../../../services'; -import { ReportingConfig } from '../../../..'; -import { CancellationToken } from '../../../../../../../plugins/reporting/common'; -import { CSV_BOM_CHARS } from '../../../../../common/constants'; -import { LevelLogger } from '../../../../lib'; -import { IndexPatternSavedObject, SavedSearchGeneratorResult } from '../../types'; +import { getFieldFormats } from '../../../services'; +import { ReportingConfig } from '../../../'; +import { CancellationToken } from '../../../../../../plugins/reporting/common'; +import { CSV_BOM_CHARS } from '../../../../common/constants'; +import { LevelLogger } from '../../../lib'; +import { IndexPatternSavedObject, SavedSearchGeneratorResult } from '../types'; import { checkIfRowsHaveFormulas } from './check_cells_for_formulas'; import { createEscapeValue } from './escape_value'; import { fieldFormatMapFactory } from './field_format_map'; diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/max_size_string_builder.test.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/max_size_string_builder.test.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/max_size_string_builder.test.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/max_size_string_builder.test.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/max_size_string_builder.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/max_size_string_builder.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/max_size_string_builder.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/max_size_string_builder.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv/index.ts b/x-pack/plugins/reporting/server/export_types/csv/index.ts index b5eacdfc62c8b..dffc874831dc2 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/index.ts @@ -15,8 +15,8 @@ import { import { CSV_JOB_TYPE as jobType } from '../../../constants'; import { ESQueueCreateJobFn, ESQueueWorkerExecuteFn, ExportTypeDefinition } from '../../types'; import { metadata } from './metadata'; -import { scheduleTaskFnFactory } from './server/create_job'; -import { runTaskFnFactory } from './server/execute_job'; +import { scheduleTaskFnFactory } from './create_job'; +import { runTaskFnFactory } from './execute_job'; import { JobParamsDiscoverCsv, ScheduledTaskParamsCSV } from './types'; export const getExportType = (): ExportTypeDefinition< diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/lib/get_request.ts b/x-pack/plugins/reporting/server/export_types/csv/lib/get_request.ts similarity index 93% rename from x-pack/plugins/reporting/server/export_types/csv/server/lib/get_request.ts rename to x-pack/plugins/reporting/server/export_types/csv/lib/get_request.ts index 21e49bd62ccc7..09e6becc2baec 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/lib/get_request.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/lib/get_request.ts @@ -7,8 +7,8 @@ import { Crypto } from '@elastic/node-crypto'; import { i18n } from '@kbn/i18n'; import Hapi from 'hapi'; -import { KibanaRequest } from '../../../../../../../../src/core/server'; -import { LevelLogger } from '../../../../lib'; +import { KibanaRequest } from '../../../../../../../src/core/server'; +import { LevelLogger } from '../../../lib'; export const getRequest = async ( headers: string | undefined, diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts similarity index 94% rename from x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job.ts rename to x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts index 96fb2033f0954..e7fb0c6e2cb99 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts @@ -7,9 +7,9 @@ import { notFound, notImplemented } from 'boom'; import { get } from 'lodash'; import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; -import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../../common/constants'; -import { cryptoFactory } from '../../../lib'; -import { ScheduleTaskFnFactory, TimeRangeParams } from '../../../types'; +import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; +import { cryptoFactory } from '../../lib'; +import { ScheduleTaskFnFactory, TimeRangeParams } from '../../types'; import { JobParamsPanelCsv, SavedObject, @@ -18,7 +18,7 @@ import { SavedSearchObjectAttributesJSON, SearchPanel, VisObjectAttributesJSON, -} from '../types'; +} from './types'; export type ImmediateCreateJobFn = ( jobParams: JobParamsPanelCsv, diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts similarity index 93% rename from x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/execute_job.ts rename to x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts index a7992c34a88f1..ffe453f996698 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts @@ -5,11 +5,11 @@ */ import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; -import { CancellationToken } from '../../../../common'; -import { CONTENT_TYPE_CSV, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../../common/constants'; -import { RunTaskFnFactory, ScheduledTaskParams, TaskRunResult } from '../../../types'; -import { createGenerateCsv } from '../../csv/server/generate_csv'; -import { JobParamsPanelCsv, SearchPanel } from '../types'; +import { CancellationToken } from '../../../common'; +import { CONTENT_TYPE_CSV, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; +import { RunTaskFnFactory, ScheduledTaskParams, TaskRunResult } from '../../types'; +import { createGenerateCsv } from '../csv/generate_csv'; +import { JobParamsPanelCsv, SearchPanel } from './types'; import { getFakeRequest } from './lib/get_fake_request'; import { getGenerateCsvParams } from './lib/get_csv_job'; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts index 9a9f445de0b13..7467f415299fa 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts @@ -15,16 +15,16 @@ import { import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../constants'; import { ExportTypeDefinition } from '../../types'; import { metadata } from './metadata'; -import { ImmediateCreateJobFn, scheduleTaskFnFactory } from './server/create_job'; -import { ImmediateExecuteFn, runTaskFnFactory } from './server/execute_job'; +import { ImmediateCreateJobFn, scheduleTaskFnFactory } from './create_job'; +import { ImmediateExecuteFn, runTaskFnFactory } from './execute_job'; import { JobParamsPanelCsv } from './types'; /* * These functions are exported to share with the API route handler that * generates csv from saved object immediately on request. */ -export { scheduleTaskFnFactory } from './server/create_job'; -export { runTaskFnFactory } from './server/execute_job'; +export { scheduleTaskFnFactory } from './create_job'; +export { runTaskFnFactory } from './execute_job'; export const getExportType = (): ExportTypeDefinition< JobParamsPanelCsv, diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_csv_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts similarity index 99% rename from x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_csv_job.test.ts rename to x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts index 3271c6fdae24d..9646d7eecd5b5 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_csv_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { JobParamsPanelCsv, SearchPanel } from '../../types'; +import { JobParamsPanelCsv, SearchPanel } from '../types'; import { getGenerateCsvParams } from './get_csv_job'; describe('Get CSV Job', () => { diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_csv_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts similarity index 96% rename from x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_csv_job.ts rename to x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts index 5f1954b80e1bc..0fc29c5b208d9 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_csv_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts @@ -11,7 +11,7 @@ import { Filter, IIndexPattern, Query, -} from '../../../../../../../../src/plugins/data/server'; +} from '../../../../../../../src/plugins/data/server'; import { DocValueFields, IndexPatternField, @@ -20,10 +20,10 @@ import { SavedSearchObjectAttributes, SearchPanel, SearchSource, -} from '../../types'; +} from '../types'; import { getDataSource } from './get_data_source'; import { getFilters } from './get_filters'; -import { GenerateCsvParams } from '../../../csv/server/generate_csv'; +import { GenerateCsvParams } from '../../csv/generate_csv'; export const getEsQueryConfig = async (config: IUiSettingsClient) => { const configs = await Promise.all([ diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_data_source.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_data_source.ts similarity index 95% rename from x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_data_source.ts rename to x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_data_source.ts index bf915696c8974..e3631b9c89724 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_data_source.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_data_source.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IndexPatternSavedObject } from '../../../csv/types'; -import { SavedObjectReference, SavedSearchObjectAttributesJSON, SearchSource } from '../../types'; +import { IndexPatternSavedObject } from '../../csv/types'; +import { SavedObjectReference, SavedSearchObjectAttributesJSON, SearchSource } from '../types'; export async function getDataSource( savedObjectsClient: any, diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_fake_request.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_fake_request.ts similarity index 90% rename from x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_fake_request.ts rename to x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_fake_request.ts index 09c58806de120..3afbaa650e6c8 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_fake_request.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_fake_request.ts @@ -6,9 +6,9 @@ import { i18n } from '@kbn/i18n'; import { KibanaRequest } from 'kibana/server'; -import { cryptoFactory, LevelLogger } from '../../../../lib'; -import { ScheduledTaskParams } from '../../../../types'; -import { JobParamsPanelCsv } from '../../types'; +import { cryptoFactory, LevelLogger } from '../../../lib'; +import { ScheduledTaskParams } from '../../../types'; +import { JobParamsPanelCsv } from '../types'; export const getFakeRequest = async ( job: ScheduledTaskParams, diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_filters.test.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.test.ts similarity index 98% rename from x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_filters.test.ts rename to x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.test.ts index b5d564d93d0d6..429b2c518cf14 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_filters.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.test.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TimeRangeParams } from '../../../../types'; -import { QueryFilter, SavedSearchObjectAttributes, SearchSourceFilter } from '../../types'; +import { TimeRangeParams } from '../../../types'; +import { QueryFilter, SavedSearchObjectAttributes, SearchSourceFilter } from '../types'; import { getFilters } from './get_filters'; interface Args { diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_filters.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.ts similarity index 95% rename from x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_filters.ts rename to x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.ts index 1258b03d3051b..a1b04cca0419d 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_filters.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.ts @@ -6,8 +6,8 @@ import { badRequest } from 'boom'; import moment from 'moment-timezone'; -import { TimeRangeParams } from '../../../../types'; -import { Filter, QueryFilter, SavedSearchObjectAttributes, SearchSourceFilter } from '../../types'; +import { TimeRangeParams } from '../../../types'; +import { Filter, QueryFilter, SavedSearchObjectAttributes, SearchSourceFilter } from '../types'; export function getFilters( indexPatternId: string, diff --git a/x-pack/plugins/reporting/server/export_types/png/server/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts similarity index 85% rename from x-pack/plugins/reporting/server/export_types/png/server/create_job/index.ts rename to x-pack/plugins/reporting/server/export_types/png/create_job/index.ts index f459b8f249c70..b63f2a09041b3 100644 --- a/x-pack/plugins/reporting/server/export_types/png/server/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { validateUrls } from '../../../../../common/validate_urls'; -import { cryptoFactory } from '../../../../lib'; -import { ESQueueCreateJobFn, ScheduleTaskFnFactory } from '../../../../types'; -import { JobParamsPNG } from '../../types'; +import { cryptoFactory } from '../../../lib'; +import { ESQueueCreateJobFn, ScheduleTaskFnFactory } from '../../../types'; +import { validateUrls } from '../../common'; +import { JobParamsPNG } from '../types'; export const scheduleTaskFnFactory: ScheduleTaskFnFactory>; diff --git a/x-pack/plugins/reporting/server/export_types/png/index.ts b/x-pack/plugins/reporting/server/export_types/png/index.ts index b708448b0f8b2..25b4dbd60535b 100644 --- a/x-pack/plugins/reporting/server/export_types/png/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/index.ts @@ -12,10 +12,10 @@ import { LICENSE_TYPE_TRIAL, PNG_JOB_TYPE as jobType, } from '../../../common/constants'; -import { ESQueueCreateJobFn, ESQueueWorkerExecuteFn, ExportTypeDefinition } from '../..//types'; +import { ESQueueCreateJobFn, ESQueueWorkerExecuteFn, ExportTypeDefinition } from '../../types'; import { metadata } from './metadata'; -import { scheduleTaskFnFactory } from './server/create_job'; -import { runTaskFnFactory } from './server/execute_job'; +import { scheduleTaskFnFactory } from './create_job'; +import { runTaskFnFactory } from './execute_job'; import { JobParamsPNG, ScheduledTaskParamsPNG } from './types'; export const getExportType = (): ExportTypeDefinition< diff --git a/x-pack/plugins/reporting/server/export_types/png/server/lib/generate_png.ts b/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts similarity index 89% rename from x-pack/plugins/reporting/server/export_types/png/server/lib/generate_png.ts rename to x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts index d7e9d0f812b37..5969b5b8abc00 100644 --- a/x-pack/plugins/reporting/server/export_types/png/server/lib/generate_png.ts +++ b/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts @@ -7,11 +7,10 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { map } from 'rxjs/operators'; -import { ReportingCore } from '../../../../'; -import { LevelLogger } from '../../../../lib'; -import { ConditionalHeaders, ScreenshotResults } from '../../../../types'; -import { LayoutParams } from '../../../common/layouts'; -import { PreserveLayout } from '../../../common/layouts/preserve_layout'; +import { ReportingCore } from '../../../'; +import { LevelLogger } from '../../../lib'; +import { LayoutParams, PreserveLayout } from '../../../lib/layouts'; +import { ConditionalHeaders, ScreenshotResults } from '../../../types'; export async function generatePngObservableFactory(reporting: ReportingCore) { const getScreenshots = await reporting.getScreenshotsObservable(); diff --git a/x-pack/plugins/reporting/server/export_types/png/types.d.ts b/x-pack/plugins/reporting/server/export_types/png/types.d.ts index 7a25f4ed8fe73..4c40f55f0f0d6 100644 --- a/x-pack/plugins/reporting/server/export_types/png/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/png/types.d.ts @@ -5,7 +5,7 @@ */ import { ScheduledTaskParams } from '../../../server/types'; -import { LayoutInstance, LayoutParams } from '../common/layouts'; +import { LayoutInstance, LayoutParams } from '../../lib/layouts'; // Job params: structure of incoming user request data export interface JobParamsPNG { diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts similarity index 86% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/create_job/index.ts rename to x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts index 76c5718249720..aa88ef863d32b 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { validateUrls } from '../../../../../common/validate_urls'; -import { cryptoFactory } from '../../../../lib'; -import { ESQueueCreateJobFn, ScheduleTaskFnFactory } from '../../../../types'; -import { JobParamsPDF } from '../../types'; +import { validateUrls } from '../../common'; +import { cryptoFactory } from '../../../lib'; +import { ESQueueCreateJobFn, ScheduleTaskFnFactory } from '../../../types'; +import { JobParamsPDF } from '../types'; export const scheduleTaskFnFactory: ScheduleTaskFnFactory ({ generatePdfObservableFactory: jest.fn() })); import * as Rx from 'rxjs'; -import { ReportingCore } from '../../../../'; -import { CancellationToken } from '../../../../../common'; -import { cryptoFactory, LevelLogger } from '../../../../lib'; -import { createMockReportingCore } from '../../../../test_helpers'; -import { ScheduledTaskParamsPDF } from '../../types'; +import { ReportingCore } from '../../../'; +import { CancellationToken } from '../../../../common'; +import { cryptoFactory, LevelLogger } from '../../../lib'; +import { createMockReportingCore } from '../../../test_helpers'; import { generatePdfObservableFactory } from '../lib/generate_pdf'; +import { ScheduledTaskParamsPDF } from '../types'; import { runTaskFnFactory } from './'; let mockReporting: ReportingCore; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts similarity index 94% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/execute_job/index.ts rename to x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts index 7f8f2f4f6906a..eb15c0a71ca3f 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts @@ -7,17 +7,17 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; -import { PDF_JOB_TYPE } from '../../../../../common/constants'; -import { ESQueueWorkerExecuteFn, RunTaskFnFactory, TaskRunResult } from '../../../../types'; +import { PDF_JOB_TYPE } from '../../../../common/constants'; +import { ESQueueWorkerExecuteFn, RunTaskFnFactory, TaskRunResult } from '../../../types'; import { decryptJobHeaders, getConditionalHeaders, getCustomLogo, getFullUrls, omitBlacklistedHeaders, -} from '../../../common/execute_job'; -import { ScheduledTaskParamsPDF } from '../../types'; +} from '../../common'; import { generatePdfObservableFactory } from '../lib/generate_pdf'; +import { ScheduledTaskParamsPDF } from '../types'; type QueuedPdfExecutorFactory = RunTaskFnFactory>; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts index 073bd38b538fb..e5115c243c697 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts @@ -14,8 +14,8 @@ import { } from '../../../common/constants'; import { ESQueueCreateJobFn, ESQueueWorkerExecuteFn, ExportTypeDefinition } from '../../types'; import { metadata } from './metadata'; -import { scheduleTaskFnFactory } from './server/create_job'; -import { runTaskFnFactory } from './server/execute_job'; +import { scheduleTaskFnFactory } from './create_job'; +import { runTaskFnFactory } from './execute_job'; import { JobParamsPDF, ScheduledTaskParamsPDF } from './types'; export const getExportType = (): ExportTypeDefinition< diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts similarity index 96% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/generate_pdf.ts rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts index 366949a033757..f2ce423566c46 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts @@ -7,10 +7,10 @@ import { groupBy } from 'lodash'; import * as Rx from 'rxjs'; import { mergeMap } from 'rxjs/operators'; -import { ReportingCore } from '../../../../'; -import { LevelLogger } from '../../../../lib'; -import { ConditionalHeaders, ScreenshotResults } from '../../../../types'; -import { createLayout, LayoutInstance, LayoutParams } from '../../../common/layouts'; +import { ReportingCore } from '../../../'; +import { LevelLogger } from '../../../lib'; +import { createLayout, LayoutInstance, LayoutParams } from '../../../lib/layouts'; +import { ConditionalHeaders, ScreenshotResults } from '../../../types'; // @ts-ignore untyped module import { pdf } from './pdf'; import { getTracker } from './tracker'; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/LICENSE_OFL.txt b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/LICENSE_OFL.txt similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/LICENSE_OFL.txt rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/LICENSE_OFL.txt diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/index.js b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/index.js similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/index.js rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/index.js diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/LICENSE.txt b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/LICENSE.txt similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/LICENSE.txt rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/LICENSE.txt diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/img/logo-grey.png b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/img/logo-grey.png similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/img/logo-grey.png rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/img/logo-grey.png diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/index.js b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/index.js rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/tracker.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/tracker.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/tracker.ts rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/tracker.ts diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/uri_encode.js b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/uri_encode.js similarity index 92% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/uri_encode.js rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/uri_encode.js index d057cfba4ef30..657af71c42c83 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/uri_encode.js +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/uri_encode.js @@ -5,7 +5,7 @@ */ import { forEach, isArray } from 'lodash'; -import { url } from '../../../../../../../../src/plugins/kibana_utils/server'; +import { url } from '../../../../../../../src/plugins/kibana_utils/server'; function toKeyValue(obj) { const parts = []; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts index 5399781a77753..cba0f41f07536 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts @@ -5,7 +5,7 @@ */ import { ScheduledTaskParams } from '../../../server/types'; -import { LayoutInstance, LayoutParams } from '../common/layouts'; +import { LayoutInstance, LayoutParams } from '../../lib/layouts'; // Job params: structure of incoming user request data, after being parsed from RISON export interface JobParamsPDF { diff --git a/x-pack/plugins/reporting/server/lib/create_queue.ts b/x-pack/plugins/reporting/server/lib/create_queue.ts index a8dcb92c55b2d..2da3d8bd47ccb 100644 --- a/x-pack/plugins/reporting/server/lib/create_queue.ts +++ b/x-pack/plugins/reporting/server/lib/create_queue.ts @@ -6,10 +6,10 @@ import { ReportingCore } from '../core'; import { JobSource, TaskRunResult } from '../types'; -import { createTaggedLogger } from './create_tagged_logger'; // TODO remove createTaggedLogger once esqueue is removed import { createWorkerFactory } from './create_worker'; // @ts-ignore import { Esqueue } from './esqueue'; +import { createTaggedLogger } from './esqueue/create_tagged_logger'; import { LevelLogger } from './level_logger'; import { ReportingStore } from './store'; diff --git a/x-pack/plugins/reporting/server/lib/create_tagged_logger.ts b/x-pack/plugins/reporting/server/lib/esqueue/create_tagged_logger.ts similarity index 95% rename from x-pack/plugins/reporting/server/lib/create_tagged_logger.ts rename to x-pack/plugins/reporting/server/lib/esqueue/create_tagged_logger.ts index 775930ec83bdf..2b97f3f25217a 100644 --- a/x-pack/plugins/reporting/server/lib/create_tagged_logger.ts +++ b/x-pack/plugins/reporting/server/lib/esqueue/create_tagged_logger.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LevelLogger } from './level_logger'; +import { LevelLogger } from '../level_logger'; export function createTaggedLogger(logger: LevelLogger, tags: string[]) { return (msg: string, additionalTags = []) => { diff --git a/x-pack/plugins/reporting/server/lib/index.ts b/x-pack/plugins/reporting/server/lib/index.ts index f5a50fca28b7a..e4adb1188e3fc 100644 --- a/x-pack/plugins/reporting/server/lib/index.ts +++ b/x-pack/plugins/reporting/server/lib/index.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export { LevelLogger } from './level_logger'; export { checkLicense } from './check_license'; export { createQueueFactory } from './create_queue'; export { cryptoFactory } from './crypto'; export { enqueueJobFactory } from './enqueue_job'; export { getExportTypesRegistry } from './export_types_registry'; -export { runValidations } from './validate'; -export { startTrace } from './trace'; +export { LevelLogger } from './level_logger'; export { ReportingStore } from './store'; +export { startTrace } from './trace'; +export { runValidations } from './validate'; diff --git a/x-pack/plugins/reporting/server/export_types/common/layouts/create_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts similarity index 94% rename from x-pack/plugins/reporting/server/export_types/common/layouts/create_layout.ts rename to x-pack/plugins/reporting/server/lib/layouts/create_layout.ts index 216a59d41cec0..921d302387edf 100644 --- a/x-pack/plugins/reporting/server/export_types/common/layouts/create_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CaptureConfig } from '../../../types'; +import { CaptureConfig } from '../../types'; import { LayoutParams, LayoutTypes } from './'; import { Layout } from './layout'; import { PreserveLayout } from './preserve_layout'; diff --git a/x-pack/plugins/reporting/server/export_types/common/layouts/index.ts b/x-pack/plugins/reporting/server/lib/layouts/index.ts similarity index 94% rename from x-pack/plugins/reporting/server/export_types/common/layouts/index.ts rename to x-pack/plugins/reporting/server/lib/layouts/index.ts index 23e4c095afe61..d46f088475222 100644 --- a/x-pack/plugins/reporting/server/export_types/common/layouts/index.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/index.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HeadlessChromiumDriver } from '../../../browsers'; -import { LevelLogger } from '../../../lib'; +import { HeadlessChromiumDriver } from '../../browsers'; +import { LevelLogger } from '../'; import { Layout } from './layout'; export { createLayout } from './create_layout'; diff --git a/x-pack/plugins/reporting/server/export_types/common/layouts/layout.ts b/x-pack/plugins/reporting/server/lib/layouts/layout.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/common/layouts/layout.ts rename to x-pack/plugins/reporting/server/lib/layouts/layout.ts diff --git a/x-pack/plugins/reporting/server/export_types/common/layouts/preserve_layout.css b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.css similarity index 100% rename from x-pack/plugins/reporting/server/export_types/common/layouts/preserve_layout.css rename to x-pack/plugins/reporting/server/lib/layouts/preserve_layout.css diff --git a/x-pack/plugins/reporting/server/export_types/common/layouts/preserve_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/common/layouts/preserve_layout.ts rename to x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts diff --git a/x-pack/plugins/reporting/server/export_types/common/layouts/print.css b/x-pack/plugins/reporting/server/lib/layouts/print.css similarity index 96% rename from x-pack/plugins/reporting/server/export_types/common/layouts/print.css rename to x-pack/plugins/reporting/server/lib/layouts/print.css index b5b6eae5e1ff6..4f1e3f4e5abd0 100644 --- a/x-pack/plugins/reporting/server/export_types/common/layouts/print.css +++ b/x-pack/plugins/reporting/server/lib/layouts/print.css @@ -110,7 +110,7 @@ discover-app .discover-table-footer { /** * 1. Reporting manually makes each visualization it wants to screenshot larger, so we need to hide * the visualizations in the other panels. We can only use properties that will be manually set in - * reporting/export_types/printable_pdf/server/lib/screenshot.js or this will also hide the visualization + * reporting/export_types/printable_pdf/lib/screenshot.js or this will also hide the visualization * we want to capture. * 2. React grid item's transform affects the visualizations, even when they are using fixed positioning. Chrome seems * to handle this fine, but firefox moves the visualizations around. diff --git a/x-pack/plugins/reporting/server/export_types/common/layouts/print_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts similarity index 91% rename from x-pack/plugins/reporting/server/export_types/common/layouts/print_layout.ts rename to x-pack/plugins/reporting/server/lib/layouts/print_layout.ts index 30c83771aa3c9..b055fae8a780d 100644 --- a/x-pack/plugins/reporting/server/export_types/common/layouts/print_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts @@ -6,10 +6,10 @@ import path from 'path'; import { EvaluateFn, SerializableOrJSHandle } from 'puppeteer'; -import { CaptureConfig } from '../../../types'; -import { HeadlessChromiumDriver } from '../../../browsers'; -import { LevelLogger } from '../../../lib'; -import { getDefaultLayoutSelectors, LayoutSelectorDictionary, Size, LayoutTypes } from './'; +import { LevelLogger } from '../'; +import { HeadlessChromiumDriver } from '../../browsers'; +import { CaptureConfig } from '../../types'; +import { getDefaultLayoutSelectors, LayoutSelectorDictionary, LayoutTypes, Size } from './'; import { Layout } from './layout'; export class PrintLayout extends Layout { diff --git a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/constants.ts b/x-pack/plugins/reporting/server/lib/screenshots/constants.ts similarity index 92% rename from x-pack/plugins/reporting/server/export_types/common/lib/screenshots/constants.ts rename to x-pack/plugins/reporting/server/lib/screenshots/constants.ts index a3faf9337524e..854763e499135 100644 --- a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/constants.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/constants.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +export const DEFAULT_PAGELOAD_SELECTOR = '.application'; + export const CONTEXT_GETNUMBEROFITEMS = 'GetNumberOfItems'; export const CONTEXT_INJECTCSS = 'InjectCss'; export const CONTEXT_WAITFORRENDER = 'WaitForRender'; diff --git a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_element_position_data.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.ts similarity index 93% rename from x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_element_position_data.ts rename to x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.ts index 140d76f8d1cd6..4fb9fd96ecfe6 100644 --- a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_element_position_data.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.ts @@ -5,10 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import { HeadlessChromiumDriver } from '../../../../browsers'; -import { LevelLogger, startTrace } from '../../../../lib'; -import { AttributesMap, ElementsPositionAndAttribute } from '../../../../types'; -import { LayoutInstance } from '../../layouts'; +import { HeadlessChromiumDriver } from '../../browsers'; +import { AttributesMap, ElementsPositionAndAttribute } from '../../types'; +import { LevelLogger, startTrace } from '../'; +import { LayoutInstance } from '../layouts'; import { CONTEXT_ELEMENTATTRIBUTES } from './constants'; export const getElementPositionAndAttributes = async ( diff --git a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_number_of_items.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.ts similarity index 91% rename from x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_number_of_items.ts rename to x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.ts index 42eb91ecba830..49c690e8c024d 100644 --- a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_number_of_items.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.ts @@ -5,10 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import { HeadlessChromiumDriver } from '../../../../browsers'; -import { LevelLogger, startTrace } from '../../../../lib'; -import { CaptureConfig } from '../../../../types'; -import { LayoutInstance } from '../../layouts'; +import { HeadlessChromiumDriver } from '../../browsers'; +import { CaptureConfig } from '../../types'; +import { LevelLogger, startTrace } from '../'; +import { LayoutInstance } from '../layouts'; import { CONTEXT_GETNUMBEROFITEMS, CONTEXT_READMETADATA } from './constants'; export const getNumberOfItems = async ( @@ -68,7 +68,6 @@ export const getNumberOfItems = async ( }, }) ); - itemsCount = 1; } endTrace(); diff --git a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_screenshots.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts similarity index 91% rename from x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_screenshots.ts rename to x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts index 05c315b8341a3..bc7b7005674a7 100644 --- a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_screenshots.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts @@ -5,9 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import { HeadlessChromiumDriver } from '../../../../browsers'; -import { LevelLogger, startTrace } from '../../../../lib'; -import { ElementsPositionAndAttribute, Screenshot } from '../../../../types'; +import { HeadlessChromiumDriver } from '../../browsers'; +import { ElementsPositionAndAttribute, Screenshot } from '../../types'; +import { LevelLogger, startTrace } from '../'; export const getScreenshots = async ( browser: HeadlessChromiumDriver, diff --git a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_time_range.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_time_range.ts similarity index 87% rename from x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_time_range.ts rename to x-pack/plugins/reporting/server/lib/screenshots/get_time_range.ts index ba68a5fec4e4c..afd6364454835 100644 --- a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_time_range.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/get_time_range.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HeadlessChromiumDriver } from '../../../../browsers'; -import { LevelLogger, startTrace } from '../../../../lib'; -import { LayoutInstance } from '../../layouts'; +import { LevelLogger, startTrace } from '../'; +import { LayoutInstance } from '../../../common/types'; +import { HeadlessChromiumDriver } from '../../browsers'; import { CONTEXT_GETTIMERANGE } from './constants'; export const getTimeRange = async ( diff --git a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/index.ts b/x-pack/plugins/reporting/server/lib/screenshots/index.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/common/lib/screenshots/index.ts rename to x-pack/plugins/reporting/server/lib/screenshots/index.ts diff --git a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/inject_css.ts b/x-pack/plugins/reporting/server/lib/screenshots/inject_css.ts similarity index 90% rename from x-pack/plugins/reporting/server/export_types/common/lib/screenshots/inject_css.ts rename to x-pack/plugins/reporting/server/lib/screenshots/inject_css.ts index d72afacc1bef3..f893951815e9e 100644 --- a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/inject_css.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/inject_css.ts @@ -7,9 +7,9 @@ import { i18n } from '@kbn/i18n'; import fs from 'fs'; import { promisify } from 'util'; -import { HeadlessChromiumDriver } from '../../../../browsers'; -import { LevelLogger, startTrace } from '../../../../lib'; -import { Layout } from '../../layouts/layout'; +import { LevelLogger, startTrace } from '../'; +import { HeadlessChromiumDriver } from '../../browsers'; +import { Layout } from '../layouts'; import { CONTEXT_INJECTCSS } from './constants'; const fsp = { readFile: promisify(fs.readFile) }; diff --git a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/observable.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts similarity index 97% rename from x-pack/plugins/reporting/server/export_types/common/lib/screenshots/observable.test.ts rename to x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts index b00233137943d..1b72be6c92f43 100644 --- a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/observable.test.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../../../../browsers/chromium/puppeteer', () => ({ +jest.mock('../../browsers/chromium/puppeteer', () => ({ puppeteerLaunch: () => ({ // Fixme needs event emitters newPage: () => ({ @@ -17,11 +17,11 @@ jest.mock('../../../../browsers/chromium/puppeteer', () => ({ import * as Rx from 'rxjs'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { loggingSystemMock } from '../../../../../../../../src/core/server/mocks'; -import { HeadlessChromiumDriver } from '../../../../browsers'; -import { LevelLogger } from '../../../../lib'; -import { createMockBrowserDriverFactory, createMockLayoutInstance } from '../../../../test_helpers'; -import { CaptureConfig, ConditionalHeaders, ElementsPositionAndAttribute } from '../../../../types'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { HeadlessChromiumDriver } from '../../browsers'; +import { LevelLogger } from '../'; +import { createMockBrowserDriverFactory, createMockLayoutInstance } from '../../test_helpers'; +import { CaptureConfig, ConditionalHeaders, ElementsPositionAndAttribute } from '../../types'; import * as contexts from './constants'; import { screenshotsObservableFactory } from './observable'; diff --git a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/observable.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts similarity index 97% rename from x-pack/plugins/reporting/server/export_types/common/lib/screenshots/observable.ts rename to x-pack/plugins/reporting/server/lib/screenshots/observable.ts index 028bff4aaa5ee..ab4dabf9ed2c2 100644 --- a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/observable.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts @@ -16,15 +16,15 @@ import { tap, toArray, } from 'rxjs/operators'; -import { HeadlessChromiumDriverFactory } from '../../../../browsers'; +import { HeadlessChromiumDriverFactory } from '../../browsers'; import { CaptureConfig, ElementsPositionAndAttribute, ScreenshotObservableOpts, ScreenshotResults, ScreenshotsObservableFn, -} from '../../../../types'; -import { DEFAULT_PAGELOAD_SELECTOR } from '../../constants'; +} from '../../types'; +import { DEFAULT_PAGELOAD_SELECTOR } from './constants'; import { getElementPositionAndAttributes } from './get_element_position_data'; import { getNumberOfItems } from './get_number_of_items'; import { getScreenshots } from './get_screenshots'; diff --git a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/open_url.ts b/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts similarity index 85% rename from x-pack/plugins/reporting/server/export_types/common/lib/screenshots/open_url.ts rename to x-pack/plugins/reporting/server/lib/screenshots/open_url.ts index bd7e8c508c118..c21ef3b91fab3 100644 --- a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/open_url.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts @@ -5,9 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import { HeadlessChromiumDriver } from '../../../../browsers'; -import { LevelLogger, startTrace } from '../../../../lib'; -import { CaptureConfig, ConditionalHeaders } from '../../../../types'; +import { HeadlessChromiumDriver } from '../../browsers'; +import { CaptureConfig, ConditionalHeaders } from '../../types'; +import { LevelLogger, startTrace } from '../'; export const openUrl = async ( captureConfig: CaptureConfig, diff --git a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/wait_for_render.ts b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_render.ts similarity index 92% rename from x-pack/plugins/reporting/server/export_types/common/lib/screenshots/wait_for_render.ts rename to x-pack/plugins/reporting/server/lib/screenshots/wait_for_render.ts index b6519e914430a..f36a7b6f73664 100644 --- a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/wait_for_render.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_render.ts @@ -5,10 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import { HeadlessChromiumDriver } from '../../../../browsers'; -import { LevelLogger, startTrace } from '../../../../lib'; -import { CaptureConfig } from '../../../../types'; -import { LayoutInstance } from '../../layouts'; +import { HeadlessChromiumDriver } from '../../browsers'; +import { CaptureConfig } from '../../types'; +import { LevelLogger, startTrace } from '../'; +import { LayoutInstance } from '../layouts'; import { CONTEXT_WAITFORRENDER } from './constants'; export const waitForRenderComplete = async ( diff --git a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/wait_for_visualizations.ts b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts similarity index 90% rename from x-pack/plugins/reporting/server/export_types/common/lib/screenshots/wait_for_visualizations.ts rename to x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts index 75a7b6516473c..779d00442522d 100644 --- a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/wait_for_visualizations.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts @@ -5,10 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import { HeadlessChromiumDriver } from '../../../../browsers'; -import { LevelLogger, startTrace } from '../../../../lib'; -import { CaptureConfig } from '../../../../types'; -import { LayoutInstance } from '../../layouts'; +import { HeadlessChromiumDriver } from '../../browsers'; +import { LevelLogger, startTrace } from '../'; +import { CaptureConfig } from '../../types'; +import { LayoutInstance } from '../layouts'; import { CONTEXT_WAITFORELEMENTSTOBEINDOM } from './constants'; type SelectorArgs = Record; diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts index 1cb964a7bbfac..0f1ed83b71767 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.ts @@ -7,8 +7,8 @@ import { ElasticsearchServiceSetup } from 'src/core/server'; import { LevelLogger } from '../'; import { ReportingCore } from '../../'; -import { LayoutInstance } from '../../export_types/common/layouts'; import { indexTimestamp } from './index_timestamp'; +import { LayoutInstance } from '../layouts'; import { mapping } from './mapping'; import { Report } from './report'; diff --git a/x-pack/plugins/reporting/server/lib/validate/index.ts b/x-pack/plugins/reporting/server/lib/validate/index.ts index 7c439d6023d5f..d20df6b7315be 100644 --- a/x-pack/plugins/reporting/server/lib/validate/index.ts +++ b/x-pack/plugins/reporting/server/lib/validate/index.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchServiceSetup } from 'kibana/server'; import { ReportingConfig } from '../../'; import { HeadlessChromiumDriverFactory } from '../../browsers/chromium/driver_factory'; -import { LevelLogger } from '../../lib'; +import { LevelLogger } from '../'; import { validateBrowser } from './validate_browser'; import { validateMaxContentLength } from './validate_max_content_length'; diff --git a/x-pack/plugins/reporting/server/lib/validate/validate_max_content_length.ts b/x-pack/plugins/reporting/server/lib/validate/validate_max_content_length.ts index 6d34937d9bd75..c38c6e5297854 100644 --- a/x-pack/plugins/reporting/server/lib/validate/validate_max_content_length.ts +++ b/x-pack/plugins/reporting/server/lib/validate/validate_max_content_length.ts @@ -8,7 +8,7 @@ import numeral from '@elastic/numeral'; import { ElasticsearchServiceSetup } from 'kibana/server'; import { defaults, get } from 'lodash'; import { ReportingConfig } from '../../'; -import { LevelLogger } from '../../lib'; +import { LevelLogger } from '../'; const KIBANA_MAX_SIZE_BYTES_PATH = 'csv.maxSizeBytes'; const ES_MAX_SIZE_BYTES_PATH = 'http.max_content_length'; diff --git a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts index 773295deea954..8250ca462049b 100644 --- a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts @@ -7,8 +7,8 @@ import { schema } from '@kbn/config-schema'; import { ReportingCore } from '../'; import { API_BASE_GENERATE_V1 } from '../../common/constants'; -import { scheduleTaskFnFactory } from '../export_types/csv_from_savedobject/server/create_job'; -import { runTaskFnFactory } from '../export_types/csv_from_savedobject/server/execute_job'; +import { scheduleTaskFnFactory } from '../export_types/csv_from_savedobject/create_job'; +import { runTaskFnFactory } from '../export_types/csv_from_savedobject/execute_job'; import { LevelLogger as Logger } from '../lib'; import { TaskRunResult } from '../types'; import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; diff --git a/x-pack/plugins/reporting/server/routes/jobs.ts b/x-pack/plugins/reporting/server/routes/jobs.ts index 90185f0736ed8..4033719b053ba 100644 --- a/x-pack/plugins/reporting/server/routes/jobs.ts +++ b/x-pack/plugins/reporting/server/routes/jobs.ts @@ -8,8 +8,8 @@ import { schema } from '@kbn/config-schema'; import Boom from 'boom'; import { ReportingCore } from '../'; import { API_BASE_URL } from '../../common/constants'; -import { jobsQueryFactory } from '../lib/jobs_query'; import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; +import { jobsQueryFactory } from './lib/jobs_query'; import { deleteJobResponseHandlerFactory, downloadJobResponseHandlerFactory, diff --git a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts index 74737b0a5d1e2..3758eafc6d718 100644 --- a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts +++ b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts @@ -6,8 +6,8 @@ import { RequestHandler, RouteMethod } from 'src/core/server'; import { AuthenticatedUser } from '../../../../security/server'; -import { getUserFactory } from '../../lib/get_user'; import { ReportingCore } from '../../core'; +import { getUserFactory } from './get_user'; type ReportingUser = AuthenticatedUser | null; const superuserRole = 'superuser'; diff --git a/x-pack/plugins/reporting/server/lib/get_user.ts b/x-pack/plugins/reporting/server/routes/lib/get_user.ts similarity index 87% rename from x-pack/plugins/reporting/server/lib/get_user.ts rename to x-pack/plugins/reporting/server/routes/lib/get_user.ts index 49d15a7c55100..fd56e8cfc28c7 100644 --- a/x-pack/plugins/reporting/server/lib/get_user.ts +++ b/x-pack/plugins/reporting/server/routes/lib/get_user.ts @@ -5,7 +5,7 @@ */ import { KibanaRequest } from 'kibana/server'; -import { SecurityPluginSetup } from '../../../security/server'; +import { SecurityPluginSetup } from '../../../../security/server'; export function getUserFactory(security?: SecurityPluginSetup) { return (request: KibanaRequest) => { diff --git a/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts b/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts index 651f1c34fee6c..df346c8b9b832 100644 --- a/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts +++ b/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts @@ -8,8 +8,8 @@ import { kibanaResponseFactory } from 'kibana/server'; import { ReportingCore } from '../../'; import { AuthenticatedUser } from '../../../../security/server'; import { WHITELISTED_JOB_CONTENT_TYPES } from '../../../common/constants'; -import { jobsQueryFactory } from '../../lib/jobs_query'; import { getDocumentPayloadFactory } from './get_document_payload'; +import { jobsQueryFactory } from './jobs_query'; interface JobResponseHandlerParams { docId: string; diff --git a/x-pack/plugins/reporting/server/lib/jobs_query.ts b/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts similarity index 96% rename from x-pack/plugins/reporting/server/lib/jobs_query.ts rename to x-pack/plugins/reporting/server/routes/lib/jobs_query.ts index f4670847260ee..f3955b4871b31 100644 --- a/x-pack/plugins/reporting/server/lib/jobs_query.ts +++ b/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts @@ -7,9 +7,9 @@ import { i18n } from '@kbn/i18n'; import { errors as elasticsearchErrors } from 'elasticsearch'; import { get } from 'lodash'; -import { ReportingCore } from '../'; -import { AuthenticatedUser } from '../../../security/server'; -import { JobSource } from '../types'; +import { ReportingCore } from '../../'; +import { AuthenticatedUser } from '../../../../security/server'; +import { JobSource } from '../../types'; const esErrors = elasticsearchErrors as Record; const defaultSize = 10; diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts index 97e22e2ca2863..db10d96db2263 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts @@ -7,8 +7,8 @@ import { Page } from 'puppeteer'; import * as Rx from 'rxjs'; import { chromium, HeadlessChromiumDriver, HeadlessChromiumDriverFactory } from '../browsers'; -import * as contexts from '../export_types/common/lib/screenshots/constants'; import { LevelLogger } from '../lib'; +import * as contexts from '../lib/screenshots/constants'; import { CaptureConfig, ElementsPositionAndAttribute } from '../types'; interface CreateMockBrowserDriverFactoryOpts { diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts index 22da9eb418e9a..c9dbbda9fd68d 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createLayout, LayoutInstance, LayoutTypes } from '../export_types/common/layouts'; +import { createLayout, LayoutInstance, LayoutTypes } from '../lib/layouts'; import { CaptureConfig } from '../types'; export const createMockLayoutInstance = (captureConfig: CaptureConfig) => { diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index 667c1546c6147..ff597b53ea0b0 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -15,8 +15,8 @@ import { SecurityPluginSetup } from '../../security/server'; import { JobStatus } from '../common/types'; import { ReportingConfigType } from './config'; import { ReportingCore } from './core'; -import { LayoutInstance } from './export_types/common/layouts'; import { LevelLogger } from './lib'; +import { LayoutInstance } from './lib/layouts'; /* * Routing / API types From c16bffc2038661dfbb8f4fc68b72dfc6c27ec89a Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 14 Jul 2020 16:49:00 -0400 Subject: [PATCH 37/82] [Ingest Manager] Copy change enroll new agent -> Add Agent (#71691) --- .../sections/agent_config/components/actions_menu.tsx | 2 +- .../ingest_manager/sections/fleet/agent_list_page/index.tsx | 2 +- .../ingest_manager/sections/fleet/components/list_layout.tsx | 2 +- .../applications/ingest_manager/sections/overview/index.tsx | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/actions_menu.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/actions_menu.tsx index 86d191d4ff904..a71de4b60c08c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/actions_menu.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/actions_menu.tsx @@ -85,7 +85,7 @@ export const AgentConfigActionMenu = memo<{ > , = () => { setIsEnrollmentFlyoutOpen(true)}> ) : null diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx index 60cbc31081302..46190033d4d6b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx @@ -112,7 +112,7 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => { setIsEnrollmentFlyoutOpen(true)}>
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx index f4b68f0c5107e..ea7ae093ee59a 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx @@ -71,7 +71,7 @@ export const IngestManagerOverview: React.FunctionComponent = () => {

@@ -84,7 +84,7 @@ export const IngestManagerOverview: React.FunctionComponent = () => { setIsEnrollmentFlyoutOpen(true)}>
From 3f95b7a1f99cb929029105c9103472ab89b20ef9 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Tue, 14 Jul 2020 17:00:35 -0400 Subject: [PATCH 38/82] adjust query to include agents without endpoint as unenrolled (#71715) --- .../server/endpoint/routes/metadata/support/unenroll.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts index bba9d921310da..136f314aa415f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts @@ -18,7 +18,8 @@ export async function findAllUnenrolledAgentIds( page: pageNum, perPage: pageSize, showInactive: true, - kuery: 'fleet-agents.packages:endpoint AND fleet-agents.active:false', + kuery: + '(fleet-agents.packages : "endpoint" AND fleet-agents.active : false) OR (NOT fleet-agents.packages : "endpoint" AND fleet-agents.active : true)', }; }; From e4546b3bf5414726e1c87823cacdcb4ec8d91ae4 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 14 Jul 2020 14:04:14 -0700 Subject: [PATCH 39/82] [tests] Temporarily skipped to promote snapshot Will be re-enabled in https://github.com/elastic/kibana/pull/71727 Signed-off-by: Tyler Smalley --- x-pack/test/api_integration/apis/fleet/setup.ts | 4 +++- .../security_solution_endpoint/apps/endpoint/policy_list.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/x-pack/test/api_integration/apis/fleet/setup.ts b/x-pack/test/api_integration/apis/fleet/setup.ts index 4fcf39886e202..317dec734568c 100644 --- a/x-pack/test/api_integration/apis/fleet/setup.ts +++ b/x-pack/test/api_integration/apis/fleet/setup.ts @@ -11,7 +11,9 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const es = getService('es'); - describe('fleet_setup', () => { + // Temporarily skipped to promote snapshot + // Re-enabled in https://github.com/elastic/kibana/pull/71727 + describe.skip('fleet_setup', () => { beforeEach(async () => { try { await es.security.deleteUser({ diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts index 57321ab4cd911..5b4a5cca108f9 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts @@ -19,7 +19,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const policyTestResources = getService('policyTestResources'); const RELATIVE_DATE_FORMAT = /\d (?:seconds|minutes) ago/i; - describe('When on the Endpoint Policy List', function () { + // Temporarily skipped to promote snapshot + // Re-enabled in https://github.com/elastic/kibana/pull/71727 + describe.skip('When on the Endpoint Policy List', function () { this.tags(['ciGroup7']); before(async () => { await pageObjects.policy.navigateToPolicyList(); From 919e0f6263978aaec7269fb3ae8e400c300d5327 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Tue, 14 Jul 2020 17:09:03 -0400 Subject: [PATCH 40/82] [Index Management] Adopt data stream API changes (#71682) --- x-pack/plugins/index_management/common/types/templates.ts | 4 ++-- .../components/template_form/template_form_schemas.tsx | 6 +++--- .../apis/management/index_management/data_streams.ts | 7 +++---- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/index_management/common/types/templates.ts b/x-pack/plugins/index_management/common/types/templates.ts index 32e254e490b2a..eda00ec819159 100644 --- a/x-pack/plugins/index_management/common/types/templates.ts +++ b/x-pack/plugins/index_management/common/types/templates.ts @@ -22,7 +22,7 @@ export interface TemplateSerialized { version?: number; priority?: number; _meta?: { [key: string]: any }; - data_stream?: { timestamp_field: string }; + data_stream?: {}; } /** @@ -46,7 +46,7 @@ export interface TemplateDeserialized { name: string; }; _meta?: { [key: string]: any }; // Composable template only - dataStream?: { timestamp_field: string }; // Composable template only + dataStream?: {}; // Composable template only _kbnMeta: { type: TemplateType; hasDatastream: boolean; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx index d8c3ad8c259fc..0d9ce57a64c84 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx @@ -136,9 +136,9 @@ export const schemas: Record = { defaultValue: false, serializer: (value) => { if (value === true) { - return { - timestamp_field: '@timestamp', - }; + // For now, ES expects an empty object when defining a data stream + // https://github.com/elastic/elasticsearch/pull/59317 + return {}; } }, deserializer: (value) => { diff --git a/x-pack/test/api_integration/apis/management/index_management/data_streams.ts b/x-pack/test/api_integration/apis/management/index_management/data_streams.ts index 0fe5dab1af52d..9f5c2a3de07bf 100644 --- a/x-pack/test/api_integration/apis/management/index_management/data_streams.ts +++ b/x-pack/test/api_integration/apis/management/index_management/data_streams.ts @@ -35,9 +35,7 @@ export default function ({ getService }: FtrProviderContext) { }, }, }, - data_stream: { - timestamp_field: '@timestamp', - }, + data_stream: {}, }, }); @@ -53,7 +51,8 @@ export default function ({ getService }: FtrProviderContext) { await deleteComposableIndexTemplate(name); }; - describe('Data streams', function () { + // Temporarily skipping tests until ES snapshot is updated + describe.skip('Data streams', function () { describe('Get', () => { const testDataStreamName = 'test-data-stream'; From 04cdb5ad6fc2ef2483dcd4c82315d8470ae0e8b0 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Tue, 14 Jul 2020 17:13:30 -0400 Subject: [PATCH 41/82] Use updated onPreAuth from Platform (#71552) * Use updated onPreAuth from Platform * Add config flag. Increase default value. * Set max connections flag default to 0 (disabled) * Don't use limiting logic on checkin route * Confirm preAuth handler only added when max > 0 Co-authored-by: Elastic Machine --- .../ingest_manager/common/constants/routes.ts | 2 + .../ingest_manager/common/types/index.ts | 1 + .../ingest_manager/server/constants/index.ts | 1 + x-pack/plugins/ingest_manager/server/index.ts | 1 + .../plugins/ingest_manager/server/plugin.ts | 4 ++ .../server/routes/agent/index.ts | 6 +- .../ingest_manager/server/routes/index.ts | 1 + .../server/routes/limited_concurrency.test.ts | 35 +++++++++ .../server/routes/limited_concurrency.ts | 72 +++++++++++++++++++ 9 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/server/routes/limited_concurrency.test.ts create mode 100644 x-pack/plugins/ingest_manager/server/routes/limited_concurrency.ts diff --git a/x-pack/plugins/ingest_manager/common/constants/routes.ts b/x-pack/plugins/ingest_manager/common/constants/routes.ts index 7c3b5a198571c..94265c3920922 100644 --- a/x-pack/plugins/ingest_manager/common/constants/routes.ts +++ b/x-pack/plugins/ingest_manager/common/constants/routes.ts @@ -11,6 +11,8 @@ export const PACKAGE_CONFIG_API_ROOT = `${API_ROOT}/package_configs`; export const AGENT_CONFIG_API_ROOT = `${API_ROOT}/agent_configs`; export const FLEET_API_ROOT = `${API_ROOT}/fleet`; +export const LIMITED_CONCURRENCY_ROUTE_TAG = 'ingest:limited-concurrency'; + // EPM API routes const EPM_PACKAGES_MANY = `${EPM_API_ROOT}/packages`; const EPM_PACKAGES_ONE = `${EPM_PACKAGES_MANY}/{pkgkey}`; diff --git a/x-pack/plugins/ingest_manager/common/types/index.ts b/x-pack/plugins/ingest_manager/common/types/index.ts index 0fce5cfa6226f..d7edc04a35799 100644 --- a/x-pack/plugins/ingest_manager/common/types/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/index.ts @@ -13,6 +13,7 @@ export interface IngestManagerConfigType { enabled: boolean; tlsCheckDisabled: boolean; pollingRequestTimeout: number; + maxConcurrentConnections: number; kibana: { host?: string; ca_sha256?: string; diff --git a/x-pack/plugins/ingest_manager/server/constants/index.ts b/x-pack/plugins/ingest_manager/server/constants/index.ts index d3c074ff2e8d0..ce81736f2e84f 100644 --- a/x-pack/plugins/ingest_manager/server/constants/index.ts +++ b/x-pack/plugins/ingest_manager/server/constants/index.ts @@ -15,6 +15,7 @@ export { AGENT_UPDATE_ACTIONS_INTERVAL_MS, INDEX_PATTERN_PLACEHOLDER_SUFFIX, // Routes + LIMITED_CONCURRENCY_ROUTE_TAG, PLUGIN_ID, EPM_API_ROUTES, DATA_STREAM_API_ROUTES, diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index 16c0b6449d1e8..6c72218abc531 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -26,6 +26,7 @@ export const config = { enabled: schema.boolean({ defaultValue: true }), tlsCheckDisabled: schema.boolean({ defaultValue: false }), pollingRequestTimeout: schema.number({ defaultValue: 60000 }), + maxConcurrentConnections: schema.number({ defaultValue: 0 }), kibana: schema.object({ host: schema.maybe(schema.string()), ca_sha256: schema.maybe(schema.string()), diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index e32533dc907b9..69af475886bb9 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -34,6 +34,7 @@ import { } from './constants'; import { registerSavedObjects, registerEncryptedSavedObjects } from './saved_objects'; import { + registerLimitedConcurrencyRoutes, registerEPMRoutes, registerPackageConfigRoutes, registerDataStreamRoutes, @@ -228,6 +229,9 @@ export class IngestManagerPlugin ); } } else { + // we currently only use this global interceptor if fleet is enabled + // since it would run this func on *every* req (other plugins, CSS, etc) + registerLimitedConcurrencyRoutes(core, config); registerAgentRoutes(router); registerEnrollmentApiKeyRoutes(router); registerInstallScriptRoutes({ diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts index d7eec50eac3cf..b85d96186f233 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts @@ -10,7 +10,7 @@ */ import { IRouter } from 'src/core/server'; -import { PLUGIN_ID, AGENT_API_ROUTES } from '../../constants'; +import { PLUGIN_ID, AGENT_API_ROUTES, LIMITED_CONCURRENCY_ROUTE_TAG } from '../../constants'; import { GetAgentsRequestSchema, GetOneAgentRequestSchema, @@ -95,7 +95,7 @@ export const registerRoutes = (router: IRouter) => { { path: AGENT_API_ROUTES.ENROLL_PATTERN, validate: PostAgentEnrollRequestSchema, - options: { tags: [] }, + options: { tags: [LIMITED_CONCURRENCY_ROUTE_TAG] }, }, postAgentEnrollHandler ); @@ -105,7 +105,7 @@ export const registerRoutes = (router: IRouter) => { { path: AGENT_API_ROUTES.ACKS_PATTERN, validate: PostAgentAcksRequestSchema, - options: { tags: [] }, + options: { tags: [LIMITED_CONCURRENCY_ROUTE_TAG] }, }, postAgentAcksHandlerBuilder({ acknowledgeAgentActions: AgentService.acknowledgeAgentActions, diff --git a/x-pack/plugins/ingest_manager/server/routes/index.ts b/x-pack/plugins/ingest_manager/server/routes/index.ts index f6b4439d8bef1..87be3a80cea96 100644 --- a/x-pack/plugins/ingest_manager/server/routes/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/index.ts @@ -14,3 +14,4 @@ export { registerRoutes as registerInstallScriptRoutes } from './install_script' export { registerRoutes as registerOutputRoutes } from './output'; export { registerRoutes as registerSettingsRoutes } from './settings'; export { registerRoutes as registerAppRoutes } from './app'; +export { registerLimitedConcurrencyRoutes } from './limited_concurrency'; diff --git a/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.test.ts b/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.test.ts new file mode 100644 index 0000000000000..a0bb8e9b86fbb --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { coreMock } from 'src/core/server/mocks'; +import { registerLimitedConcurrencyRoutes } from './limited_concurrency'; +import { IngestManagerConfigType } from '../index'; + +describe('registerLimitedConcurrencyRoutes', () => { + test(`doesn't call registerOnPreAuth if maxConcurrentConnections is 0`, async () => { + const mockSetup = coreMock.createSetup(); + const mockConfig = { fleet: { maxConcurrentConnections: 0 } } as IngestManagerConfigType; + registerLimitedConcurrencyRoutes(mockSetup, mockConfig); + + expect(mockSetup.http.registerOnPreAuth).not.toHaveBeenCalled(); + }); + + test(`calls registerOnPreAuth once if maxConcurrentConnections is 1`, async () => { + const mockSetup = coreMock.createSetup(); + const mockConfig = { fleet: { maxConcurrentConnections: 1 } } as IngestManagerConfigType; + registerLimitedConcurrencyRoutes(mockSetup, mockConfig); + + expect(mockSetup.http.registerOnPreAuth).toHaveBeenCalledTimes(1); + }); + + test(`calls registerOnPreAuth once if maxConcurrentConnections is 1000`, async () => { + const mockSetup = coreMock.createSetup(); + const mockConfig = { fleet: { maxConcurrentConnections: 1000 } } as IngestManagerConfigType; + registerLimitedConcurrencyRoutes(mockSetup, mockConfig); + + expect(mockSetup.http.registerOnPreAuth).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.ts b/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.ts new file mode 100644 index 0000000000000..ec8e2f6c8d436 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + CoreSetup, + KibanaRequest, + LifecycleResponseFactory, + OnPreAuthToolkit, +} from 'kibana/server'; +import { LIMITED_CONCURRENCY_ROUTE_TAG } from '../../common'; +import { IngestManagerConfigType } from '../index'; +class MaxCounter { + constructor(private readonly max: number = 1) {} + private counter = 0; + valueOf() { + return this.counter; + } + increase() { + if (this.counter < this.max) { + this.counter += 1; + } + } + decrease() { + if (this.counter > 0) { + this.counter -= 1; + } + } + lessThanMax() { + return this.counter < this.max; + } +} + +function shouldHandleRequest(request: KibanaRequest) { + const tags = request.route.options.tags; + return tags.includes(LIMITED_CONCURRENCY_ROUTE_TAG); +} + +export function registerLimitedConcurrencyRoutes(core: CoreSetup, config: IngestManagerConfigType) { + const max = config.fleet.maxConcurrentConnections; + if (!max) return; + + const counter = new MaxCounter(max); + core.http.registerOnPreAuth(function preAuthHandler( + request: KibanaRequest, + response: LifecycleResponseFactory, + toolkit: OnPreAuthToolkit + ) { + if (!shouldHandleRequest(request)) { + return toolkit.next(); + } + + if (!counter.lessThanMax()) { + return response.customError({ + body: 'Too Many Requests', + statusCode: 429, + }); + } + + counter.increase(); + + // requests.events.aborted$ has a bug (but has test which explicitly verifies) where it's fired even when the request completes + // https://github.com/elastic/kibana/pull/70495#issuecomment-656288766 + request.events.aborted$.toPromise().then(() => { + counter.decrease(); + }); + + return toolkit.next(); + }); +} From f5259ed373e755b2c3431eb1263ec0c1acae025d Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 14 Jul 2020 15:18:17 -0600 Subject: [PATCH 42/82] [Security solution] [Hosts] Endpoint overview on host details page (#71466) --- .../public/graphql/introspection.json | 79 ++++- .../security_solution/public/graphql/types.ts | 34 ++- .../hosts/overview/host_overview.gql_query.ts | 5 + .../endpoint_overview/index.test.tsx | 48 +++ .../host_overview/endpoint_overview/index.tsx | 90 ++++++ .../endpoint_overview/translations.ts | 28 ++ .../components/host_overview/index.test.tsx | 1 - .../components/host_overview/index.tsx | 275 ++++++++++-------- .../server/endpoint/routes/metadata/index.ts | 2 +- .../server/graphql/hosts/schema.gql.ts | 17 +- .../security_solution/server/graphql/types.ts | 78 ++++- .../server/lib/compose/kibana.ts | 6 +- .../lib/hosts/elasticsearch_adapter.test.ts | 25 +- .../server/lib/hosts/elasticsearch_adapter.ts | 57 +++- .../server/lib/hosts/mock.ts | 66 +++++ .../security_solution/server/plugin.ts | 13 +- .../apis/security_solution/hosts.ts | 1 + 17 files changed, 669 insertions(+), 156 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx create mode 100644 x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/translations.ts diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 43c478ff120a0..4716440c36e61 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -6525,26 +6525,26 @@ "deprecationReason": null }, { - "name": "lastSeen", + "name": "cloud", "description": "", "args": [], - "type": { "kind": "SCALAR", "name": "Date", "ofType": null }, + "type": { "kind": "OBJECT", "name": "CloudFields", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "host", + "name": "endpoint", "description": "", "args": [], - "type": { "kind": "OBJECT", "name": "HostEcsFields", "ofType": null }, + "type": { "kind": "OBJECT", "name": "EndpointFields", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "cloud", + "name": "host", "description": "", "args": [], - "type": { "kind": "OBJECT", "name": "CloudFields", "ofType": null }, + "type": { "kind": "OBJECT", "name": "HostEcsFields", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, @@ -6555,6 +6555,14 @@ "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "lastSeen", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Date", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -6659,6 +6667,65 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "EndpointFields", + "description": "", + "fields": [ + { + "name": "endpointPolicy", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sensorVersion", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "policyStatus", + "description": "", + "args": [], + "type": { "kind": "ENUM", "name": "HostPolicyResponseActionStatus", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "HostPolicyResponseActionStatus", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "success", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "failure", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "warning", "description": "", "isDeprecated": false, "deprecationReason": null } + ], + "possibleTypes": null + }, { "kind": "OBJECT", "name": "FirstLastSeenHost", diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 084d1a63fec75..98addf3317ff4 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -301,6 +301,12 @@ export enum HostsFields { lastSeen = 'lastSeen', } +export enum HostPolicyResponseActionStatus { + success = 'success', + failure = 'failure', + warning = 'warning', +} + export enum UsersFields { name = 'name', count = 'count', @@ -1442,13 +1448,15 @@ export interface HostsEdges { export interface HostItem { _id?: Maybe; - lastSeen?: Maybe; + cloud?: Maybe; - host?: Maybe; + endpoint?: Maybe; - cloud?: Maybe; + host?: Maybe; inspect?: Maybe; + + lastSeen?: Maybe; } export interface CloudFields { @@ -1469,6 +1477,14 @@ export interface CloudMachine { type?: Maybe<(Maybe)[]>; } +export interface EndpointFields { + endpointPolicy?: Maybe; + + sensorVersion?: Maybe; + + policyStatus?: Maybe; +} + export interface FirstLastSeenHost { inspect?: Maybe; @@ -3044,6 +3060,8 @@ export namespace GetHostOverviewQuery { cloud: Maybe; inspect: Maybe; + + endpoint: Maybe; }; export type Host = { @@ -3107,6 +3125,16 @@ export namespace GetHostOverviewQuery { response: string[]; }; + + export type Endpoint = { + __typename?: 'EndpointFields'; + + endpointPolicy: Maybe; + + policyStatus: Maybe; + + sensorVersion: Maybe; + }; } export namespace GetKpiHostDetailsQuery { diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/host_overview.gql_query.ts b/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/host_overview.gql_query.ts index 46794816dbf2a..89937d0adf81e 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/host_overview.gql_query.ts +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/host_overview.gql_query.ts @@ -46,6 +46,11 @@ export const HostOverviewQuery = gql` dsl response } + endpoint { + endpointPolicy + policyStatus + sensorVersion + } } } } diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx new file mode 100644 index 0000000000000..8e221445a95d3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount } from 'enzyme'; +import React from 'react'; +import { TestProviders } from '../../../../common/mock'; + +import { EndpointOverview } from './index'; +import { HostPolicyResponseActionStatus } from '../../../../graphql/types'; + +describe('EndpointOverview Component', () => { + test('it renders with endpoint data', () => { + const endpointData = { + endpointPolicy: 'demo', + policyStatus: HostPolicyResponseActionStatus.success, + sensorVersion: '7.9.0-SNAPSHOT', + }; + const wrapper = mount( + + + + ); + + const findData = wrapper.find( + 'dl[data-test-subj="endpoint-overview"] dd.euiDescriptionList__description' + ); + expect(findData.at(0).text()).toEqual(endpointData.endpointPolicy); + expect(findData.at(1).text()).toEqual(endpointData.policyStatus); + expect(findData.at(2).text()).toContain(endpointData.sensorVersion); // contain because drag adds a space + }); + test('it renders with null data', () => { + const wrapper = mount( + + + + ); + + const findData = wrapper.find( + 'dl[data-test-subj="endpoint-overview"] dd.euiDescriptionList__description' + ); + expect(findData.at(0).text()).toEqual('—'); + expect(findData.at(1).text()).toEqual('—'); + expect(findData.at(2).text()).toContain('—'); // contain because drag adds a space + }); +}); diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx new file mode 100644 index 0000000000000..df06c2eb36837 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexItem, EuiHealth } from '@elastic/eui'; +import { getOr } from 'lodash/fp'; +import React, { useCallback, useMemo } from 'react'; + +import { DescriptionList } from '../../../../../common/utility_types'; +import { getEmptyTagValue } from '../../../../common/components/empty_value'; +import { DefaultFieldRenderer } from '../../../../timelines/components/field_renderers/field_renderers'; +import { EndpointFields, HostPolicyResponseActionStatus } from '../../../../graphql/types'; +import { DescriptionListStyled } from '../../../../common/components/page'; + +import * as i18n from './translations'; + +interface Props { + data: EndpointFields | null; +} + +const getDescriptionList = (descriptionList: DescriptionList[], key: number) => ( + + + +); + +export const EndpointOverview = React.memo(({ data }) => { + const getDefaultRenderer = useCallback( + (fieldName: string, fieldData: EndpointFields, attrName: string) => ( + + ), + [] + ); + const descriptionLists: Readonly = useMemo( + () => [ + [ + { + title: i18n.ENDPOINT_POLICY, + description: + data != null && data.endpointPolicy != null ? data.endpointPolicy : getEmptyTagValue(), + }, + ], + [ + { + title: i18n.POLICY_STATUS, + description: + data != null && data.policyStatus != null ? ( + + {data.policyStatus} + + ) : ( + getEmptyTagValue() + ), + }, + ], + [ + { + title: i18n.SENSORVERSION, + description: + data != null && data.sensorVersion != null + ? getDefaultRenderer('sensorVersion', data, 'agent.version') + : getEmptyTagValue(), + }, + ], + [], // needs 4 columns for design + ], + [data, getDefaultRenderer] + ); + + return ( + <> + {descriptionLists.map((descriptionList, index) => getDescriptionList(descriptionList, index))} + + ); +}); + +EndpointOverview.displayName = 'EndpointOverview'; diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/translations.ts b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/translations.ts new file mode 100644 index 0000000000000..34e3347b5ff9a --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/translations.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ENDPOINT_POLICY = i18n.translate( + 'xpack.securitySolution.host.details.endpoint.endpointPolicy', + { + defaultMessage: 'Endpoint policy', + } +); + +export const POLICY_STATUS = i18n.translate( + 'xpack.securitySolution.host.details.endpoint.policyStatus', + { + defaultMessage: 'Policy status', + } +); + +export const SENSORVERSION = i18n.translate( + 'xpack.securitySolution.host.details.endpoint.sensorversion', + { + defaultMessage: 'Sensorversion', + } +); diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx index 56c232158ac02..0286961fd78af 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx @@ -11,7 +11,6 @@ import { TestProviders } from '../../../common/mock'; import { HostOverview } from './index'; import { mockData } from './mock'; import { mockAnomalies } from '../../../common/components/ml/mock'; - describe('Host Summary Component', () => { describe('rendering', () => { test('it renders the default Host Summary', () => { diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx index c1004f772a0ee..0c679cc94f787 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexItem } from '@elastic/eui'; +import { EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; import { getOr } from 'lodash/fp'; -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import { DEFAULT_DARK_MODE } from '../../../../common/constants'; import { DescriptionList } from '../../../../common/utility_types'; @@ -33,6 +33,7 @@ import { } from '../../../hosts/components/first_last_seen_host'; import * as i18n from './translations'; +import { EndpointOverview } from './endpoint_overview'; interface HostSummaryProps { data: HostItem; @@ -53,143 +54,183 @@ const getDescriptionList = (descriptionList: DescriptionList[], key: number) => export const HostOverview = React.memo( ({ + anomaliesData, data, - loading, - id, - startDate, endDate, + id, isLoadingAnomaliesData, - anomaliesData, + loading, narrowDateRange, + startDate, }) => { const capabilities = useMlCapabilities(); const userPermissions = hasMlUserPermissions(capabilities); const [darkMode] = useUiSetting$(DEFAULT_DARK_MODE); - const getDefaultRenderer = (fieldName: string, fieldData: HostItem) => ( - + const getDefaultRenderer = useCallback( + (fieldName: string, fieldData: HostItem) => ( + + ), + [] ); - const column: DescriptionList[] = [ - { - title: i18n.HOST_ID, - description: data.host - ? hostIdRenderer({ host: data.host, noLink: true }) - : getEmptyTagValue(), - }, - { - title: i18n.FIRST_SEEN, - description: - data.host != null && data.host.name && data.host.name.length ? ( - - ) : ( - getEmptyTagValue() - ), - }, - { - title: i18n.LAST_SEEN, - description: - data.host != null && data.host.name && data.host.name.length ? ( - - ) : ( - getEmptyTagValue() - ), - }, - ]; - const firstColumn = userPermissions - ? [ - ...column, - { - title: i18n.MAX_ANOMALY_SCORE_BY_JOB, - description: ( - - ), - }, - ] - : column; - - const descriptionLists: Readonly = [ - firstColumn, - [ + const column: DescriptionList[] = useMemo( + () => [ { - title: i18n.IP_ADDRESSES, - description: ( - (ip != null ? : getEmptyTagValue())} - /> - ), + title: i18n.HOST_ID, + description: data.host + ? hostIdRenderer({ host: data.host, noLink: true }) + : getEmptyTagValue(), }, { - title: i18n.MAC_ADDRESSES, - description: getDefaultRenderer('host.mac', data), - }, - { title: i18n.PLATFORM, description: getDefaultRenderer('host.os.platform', data) }, - ], - [ - { title: i18n.OS, description: getDefaultRenderer('host.os.name', data) }, - { title: i18n.FAMILY, description: getDefaultRenderer('host.os.family', data) }, - { title: i18n.VERSION, description: getDefaultRenderer('host.os.version', data) }, - { title: i18n.ARCHITECTURE, description: getDefaultRenderer('host.architecture', data) }, - ], - [ - { - title: i18n.CLOUD_PROVIDER, - description: getDefaultRenderer('cloud.provider', data), - }, - { - title: i18n.REGION, - description: getDefaultRenderer('cloud.region', data), - }, - { - title: i18n.INSTANCE_ID, - description: getDefaultRenderer('cloud.instance.id', data), + title: i18n.FIRST_SEEN, + description: + data.host != null && data.host.name && data.host.name.length ? ( + + ) : ( + getEmptyTagValue() + ), }, { - title: i18n.MACHINE_TYPE, - description: getDefaultRenderer('cloud.machine.type', data), + title: i18n.LAST_SEEN, + description: + data.host != null && data.host.name && data.host.name.length ? ( + + ) : ( + getEmptyTagValue() + ), }, ], - ]; + [data] + ); + const firstColumn = useMemo( + () => + userPermissions + ? [ + ...column, + { + title: i18n.MAX_ANOMALY_SCORE_BY_JOB, + description: ( + + ), + }, + ] + : column, + [ + anomaliesData, + column, + endDate, + isLoadingAnomaliesData, + narrowDateRange, + startDate, + userPermissions, + ] + ); + const descriptionLists: Readonly = useMemo( + () => [ + firstColumn, + [ + { + title: i18n.IP_ADDRESSES, + description: ( + (ip != null ? : getEmptyTagValue())} + /> + ), + }, + { + title: i18n.MAC_ADDRESSES, + description: getDefaultRenderer('host.mac', data), + }, + { title: i18n.PLATFORM, description: getDefaultRenderer('host.os.platform', data) }, + ], + [ + { title: i18n.OS, description: getDefaultRenderer('host.os.name', data) }, + { title: i18n.FAMILY, description: getDefaultRenderer('host.os.family', data) }, + { title: i18n.VERSION, description: getDefaultRenderer('host.os.version', data) }, + { title: i18n.ARCHITECTURE, description: getDefaultRenderer('host.architecture', data) }, + ], + [ + { + title: i18n.CLOUD_PROVIDER, + description: getDefaultRenderer('cloud.provider', data), + }, + { + title: i18n.REGION, + description: getDefaultRenderer('cloud.region', data), + }, + { + title: i18n.INSTANCE_ID, + description: getDefaultRenderer('cloud.instance.id', data), + }, + { + title: i18n.MACHINE_TYPE, + description: getDefaultRenderer('cloud.machine.type', data), + }, + ], + ], + [data, firstColumn, getDefaultRenderer] + ); return ( - - - + <> + + + + + {descriptionLists.map((descriptionList, index) => + getDescriptionList(descriptionList, index) + )} - {descriptionLists.map((descriptionList, index) => - getDescriptionList(descriptionList, index) - )} + {loading && ( + + )} + + + {data.endpoint != null ? ( + <> + + + - {loading && ( - - )} - - + {loading && ( + + )} + + + ) : null} + ); } ); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts index 7915f1a8cbf50..cb9889ca0cb76 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts @@ -7,8 +7,8 @@ import { IRouter, Logger, RequestHandlerContext } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { schema } from '@kbn/config-schema'; - import Boom from 'boom'; + import { metadataIndexPattern } from '../../../../common/endpoint/constants'; import { getESQueryHostMetadataByID, kibanaRequestToMetadataListESQuery } from './query_builders'; import { diff --git a/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts index d813a08cad6db..02f8341cd6fd9 100644 --- a/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts @@ -41,12 +41,25 @@ export const hostsSchema = gql` region: [String] } + enum HostPolicyResponseActionStatus { + success + failure + warning + } + + type EndpointFields { + endpointPolicy: String + sensorVersion: String + policyStatus: HostPolicyResponseActionStatus + } + type HostItem { _id: String - lastSeen: Date - host: HostEcsFields cloud: CloudFields + endpoint: EndpointFields + host: HostEcsFields inspect: Inspect + lastSeen: Date } type HostsEdges { diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index 668266cc67c3a..1eaf47ad43812 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -303,6 +303,12 @@ export enum HostsFields { lastSeen = 'lastSeen', } +export enum HostPolicyResponseActionStatus { + success = 'success', + failure = 'failure', + warning = 'warning', +} + export enum UsersFields { name = 'name', count = 'count', @@ -1444,13 +1450,15 @@ export interface HostsEdges { export interface HostItem { _id?: Maybe; - lastSeen?: Maybe; + cloud?: Maybe; - host?: Maybe; + endpoint?: Maybe; - cloud?: Maybe; + host?: Maybe; inspect?: Maybe; + + lastSeen?: Maybe; } export interface CloudFields { @@ -1471,6 +1479,14 @@ export interface CloudMachine { type?: Maybe<(Maybe)[]>; } +export interface EndpointFields { + endpointPolicy?: Maybe; + + sensorVersion?: Maybe; + + policyStatus?: Maybe; +} + export interface FirstLastSeenHost { inspect?: Maybe; @@ -6325,13 +6341,15 @@ export namespace HostItemResolvers { export interface Resolvers { _id?: _IdResolver, TypeParent, TContext>; - lastSeen?: LastSeenResolver, TypeParent, TContext>; + cloud?: CloudResolver, TypeParent, TContext>; - host?: HostResolver, TypeParent, TContext>; + endpoint?: EndpointResolver, TypeParent, TContext>; - cloud?: CloudResolver, TypeParent, TContext>; + host?: HostResolver, TypeParent, TContext>; inspect?: InspectResolver, TypeParent, TContext>; + + lastSeen?: LastSeenResolver, TypeParent, TContext>; } export type _IdResolver, Parent = HostItem, TContext = SiemContext> = Resolver< @@ -6339,18 +6357,18 @@ export namespace HostItemResolvers { Parent, TContext >; - export type LastSeenResolver< - R = Maybe, + export type CloudResolver< + R = Maybe, Parent = HostItem, TContext = SiemContext > = Resolver; - export type HostResolver< - R = Maybe, + export type EndpointResolver< + R = Maybe, Parent = HostItem, TContext = SiemContext > = Resolver; - export type CloudResolver< - R = Maybe, + export type HostResolver< + R = Maybe, Parent = HostItem, TContext = SiemContext > = Resolver; @@ -6359,6 +6377,11 @@ export namespace HostItemResolvers { Parent = HostItem, TContext = SiemContext > = Resolver; + export type LastSeenResolver< + R = Maybe, + Parent = HostItem, + TContext = SiemContext + > = Resolver; } export namespace CloudFieldsResolvers { @@ -6418,6 +6441,36 @@ export namespace CloudMachineResolvers { > = Resolver; } +export namespace EndpointFieldsResolvers { + export interface Resolvers { + endpointPolicy?: EndpointPolicyResolver, TypeParent, TContext>; + + sensorVersion?: SensorVersionResolver, TypeParent, TContext>; + + policyStatus?: PolicyStatusResolver< + Maybe, + TypeParent, + TContext + >; + } + + export type EndpointPolicyResolver< + R = Maybe, + Parent = EndpointFields, + TContext = SiemContext + > = Resolver; + export type SensorVersionResolver< + R = Maybe, + Parent = EndpointFields, + TContext = SiemContext + > = Resolver; + export type PolicyStatusResolver< + R = Maybe, + Parent = EndpointFields, + TContext = SiemContext + > = Resolver; +} + export namespace FirstLastSeenHostResolvers { export interface Resolvers { inspect?: InspectResolver, TypeParent, TContext>; @@ -9331,6 +9384,7 @@ export type IResolvers = { CloudFields?: CloudFieldsResolvers.Resolvers; CloudInstance?: CloudInstanceResolvers.Resolvers; CloudMachine?: CloudMachineResolvers.Resolvers; + EndpointFields?: EndpointFieldsResolvers.Resolvers; FirstLastSeenHost?: FirstLastSeenHostResolvers.Resolvers; IpOverviewData?: IpOverviewDataResolvers.Resolvers; Overview?: OverviewResolvers.Resolvers; diff --git a/x-pack/plugins/security_solution/server/lib/compose/kibana.ts b/x-pack/plugins/security_solution/server/lib/compose/kibana.ts index 8bc90bed25168..db76f6d52dbb0 100644 --- a/x-pack/plugins/security_solution/server/lib/compose/kibana.ts +++ b/x-pack/plugins/security_solution/server/lib/compose/kibana.ts @@ -32,11 +32,13 @@ import * as note from '../note/saved_object'; import * as pinnedEvent from '../pinned_event/saved_object'; import * as timeline from '../timeline/saved_object'; import { ElasticsearchMatrixHistogramAdapter, MatrixHistogram } from '../matrix_histogram'; +import { EndpointAppContext } from '../../endpoint/types'; export function compose( core: CoreSetup, plugins: SetupPlugins, - isProductionMode: boolean + isProductionMode: boolean, + endpointContext: EndpointAppContext ): AppBackendLibs { const framework = new KibanaBackendFrameworkAdapter(core, plugins, isProductionMode); const sources = new Sources(new ConfigurationSourcesAdapter()); @@ -46,7 +48,7 @@ export function compose( authentications: new Authentications(new ElasticsearchAuthenticationAdapter(framework)), events: new Events(new ElasticsearchEventsAdapter(framework)), fields: new IndexFields(new ElasticsearchIndexFieldAdapter(framework)), - hosts: new Hosts(new ElasticsearchHostsAdapter(framework)), + hosts: new Hosts(new ElasticsearchHostsAdapter(framework, endpointContext)), ipDetails: new IpDetails(new ElasticsearchIpDetailsAdapter(framework)), tls: new TLS(new ElasticsearchTlsAdapter(framework)), kpiHosts: new KpiHosts(new ElasticsearchKpiHostsAdapter(framework)), diff --git a/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.test.ts b/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.test.ts index 20510e1089f96..766fbd5dca031 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.test.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.test.ts @@ -9,6 +9,7 @@ import { FrameworkAdapter, FrameworkRequest } from '../framework'; import { ElasticsearchHostsAdapter, formatHostEdgesData } from './elasticsearch_adapter'; import { + mockEndpointMetadata, mockGetHostOverviewOptions, mockGetHostOverviewRequest, mockGetHostOverviewResponse, @@ -26,6 +27,10 @@ import { mockGetHostsQueryDsl, } from './mock'; import { HostAggEsItem } from './types'; +import { EndpointAppContext } from '../../endpoint/types'; +import { mockLogger } from '../detection_engine/signals/__mocks__/es_results'; +import { EndpointAppContextService } from '../../endpoint/endpoint_app_context_services'; +import { createMockEndpointAppContextServiceStartContract } from '../../endpoint/mocks'; jest.mock('./query.hosts.dsl', () => { return { @@ -44,6 +49,11 @@ jest.mock('./query.last_first_seen_host.dsl', () => { buildLastFirstSeenHostQuery: jest.fn(() => mockGetHostLastFirstSeenDsl), }; }); +jest.mock('../../endpoint/routes/metadata', () => { + return { + getHostData: jest.fn(() => mockEndpointMetadata), + }; +}); describe('hosts elasticsearch_adapter', () => { describe('#formatHostsData', () => { @@ -155,6 +165,15 @@ describe('hosts elasticsearch_adapter', () => { }); }); + const endpointAppContextService = new EndpointAppContextService(); + const startContract = createMockEndpointAppContextServiceStartContract(); + endpointAppContextService.start(startContract); + + const endpointContext: EndpointAppContext = { + logFactory: mockLogger, + service: endpointAppContextService, + config: jest.fn(), + }; describe('#getHosts', () => { const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockGetHostsResponse); @@ -166,7 +185,7 @@ describe('hosts elasticsearch_adapter', () => { jest.doMock('../framework', () => ({ callWithRequest: mockCallWithRequest })); test('Happy Path', async () => { - const EsHosts = new ElasticsearchHostsAdapter(mockFramework); + const EsHosts = new ElasticsearchHostsAdapter(mockFramework, endpointContext); const data: HostsData = await EsHosts.getHosts( mockGetHostsRequest as FrameworkRequest, mockGetHostsOptions @@ -186,7 +205,7 @@ describe('hosts elasticsearch_adapter', () => { jest.doMock('../framework', () => ({ callWithRequest: mockCallWithRequest })); test('Happy Path', async () => { - const EsHosts = new ElasticsearchHostsAdapter(mockFramework); + const EsHosts = new ElasticsearchHostsAdapter(mockFramework, endpointContext); const data: HostItem = await EsHosts.getHostOverview( mockGetHostOverviewRequest as FrameworkRequest, mockGetHostOverviewOptions @@ -206,7 +225,7 @@ describe('hosts elasticsearch_adapter', () => { jest.doMock('../framework', () => ({ callWithRequest: mockCallWithRequest })); test('Happy Path', async () => { - const EsHosts = new ElasticsearchHostsAdapter(mockFramework); + const EsHosts = new ElasticsearchHostsAdapter(mockFramework, endpointContext); const data: FirstLastSeenHost = await EsHosts.getHostFirstLastSeen( mockGetHostLastFirstSeenRequest as FrameworkRequest, mockGetHostLastFirstSeenOptions diff --git a/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.ts b/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.ts index 90ac44ab3cb46..796338e189d60 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.ts @@ -6,12 +6,17 @@ import { get, getOr, has, head, set } from 'lodash/fp'; -import { FirstLastSeenHost, HostItem, HostsData, HostsEdges } from '../../graphql/types'; +import { + FirstLastSeenHost, + HostItem, + HostsData, + HostsEdges, + EndpointFields, +} from '../../graphql/types'; import { inspectStringifyObject } from '../../utils/build_query'; import { hostFieldsMap } from '../ecs_fields'; import { FrameworkAdapter, FrameworkRequest } from '../framework'; import { TermAggregation } from '../types'; - import { buildHostOverviewQuery } from './query.detail_host.dsl'; import { buildHostsQuery } from './query.hosts.dsl'; import { buildLastFirstSeenHostQuery } from './query.last_first_seen_host.dsl'; @@ -27,9 +32,14 @@ import { HostValue, } from './types'; import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants'; +import { EndpointAppContext } from '../../endpoint/types'; +import { getHostData } from '../../endpoint/routes/metadata'; export class ElasticsearchHostsAdapter implements HostsAdapter { - constructor(private readonly framework: FrameworkAdapter) {} + constructor( + private readonly framework: FrameworkAdapter, + private readonly endpointContext: EndpointAppContext + ) {} public async getHosts( request: FrameworkRequest, @@ -83,8 +93,47 @@ export class ElasticsearchHostsAdapter implements HostsAdapter { dsl: [inspectStringifyObject(dsl)], response: [inspectStringifyObject(response)], }; + const formattedHostItem = formatHostItem(options.fields, aggregations); + const hostId = + formattedHostItem.host && formattedHostItem.host.id + ? Array.isArray(formattedHostItem.host.id) + ? formattedHostItem.host.id[0] + : formattedHostItem.host.id + : null; + const endpoint: EndpointFields | null = await this.getHostEndpoint(request, hostId); + return { inspect, _id: options.hostName, ...formattedHostItem, endpoint }; + } - return { inspect, _id: options.hostName, ...formatHostItem(options.fields, aggregations) }; + public async getHostEndpoint( + request: FrameworkRequest, + hostId: string | null + ): Promise { + const logger = this.endpointContext.logFactory.get('metadata'); + try { + const agentService = this.endpointContext.service.getAgentService(); + if (agentService === undefined) { + throw new Error('agentService not available'); + } + const metadataRequestContext = { + agentService, + logger, + requestHandlerContext: request.context, + }; + const endpointData = + hostId != null && metadataRequestContext.agentService != null + ? await getHostData(metadataRequestContext, hostId) + : null; + return endpointData != null && endpointData.metadata + ? { + endpointPolicy: endpointData.metadata.Endpoint.policy.applied.name, + policyStatus: endpointData.metadata.Endpoint.policy.applied.status, + sensorVersion: endpointData.metadata.agent.version, + } + : null; + } catch (err) { + logger.warn(JSON.stringify(err, null, 2)); + return null; + } } public async getHostFirstLastSeen( diff --git a/x-pack/plugins/security_solution/server/lib/hosts/mock.ts b/x-pack/plugins/security_solution/server/lib/hosts/mock.ts index 30082990b55f9..0f6bc5c1b0e0c 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/mock.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/mock.ts @@ -497,6 +497,11 @@ export const mockGetHostOverviewResult = { provider: ['gce'], region: ['us-east-1'], }, + endpoint: { + endpointPolicy: 'demo', + policyStatus: 'success', + sensorVersion: '7.9.0-SNAPSHOT', + }, }; export const mockGetHostLastFirstSeenOptions: HostLastFirstSeenRequestOptions = { @@ -564,3 +569,64 @@ export const mockGetHostLastFirstSeenResult = { firstSeen: '2019-02-22T03:41:32.826Z', lastSeen: '2019-04-09T16:18:12.178Z', }; + +export const mockEndpointMetadata = { + metadata: { + '@timestamp': '2020-07-13T01:08:37.68896700Z', + Endpoint: { + policy: { + applied: { id: '3de86380-aa5a-11ea-b969-0bee1b260ab8', name: 'demo', status: 'success' }, + }, + status: 'enrolled', + }, + agent: { + build: { + original: + 'version: 7.9.0-SNAPSHOT, compiled: Thu Jul 09 07:56:12 2020, branch: 7.x, commit: 713a1071de475f15b3a1f0944d3602ed532597a5', + }, + id: 'c29e0de1-7476-480b-b242-38f0394bf6a1', + type: 'endpoint', + version: '7.9.0-SNAPSHOT', + }, + dataset: { name: 'endpoint.metadata', namespace: 'default', type: 'metrics' }, + ecs: { version: '1.5.0' }, + elastic: { agent: { id: '' } }, + event: { + action: 'endpoint_metadata', + category: ['host'], + created: '2020-07-13T01:08:37.68896700Z', + dataset: 'endpoint.metadata', + id: 'Lkio+AHbZGSPFb7q++++++2E', + kind: 'metric', + module: 'endpoint', + sequence: 146, + type: ['info'], + }, + host: { + architecture: 'x86_64', + hostname: 'DESKTOP-4I1B23J', + id: 'a4148b63-1758-ab1f-a6d3-f95075cb1a9c', + ip: [ + '172.16.166.129', + 'fe80::c07e:eee9:3e8d:ea6d', + '169.254.205.96', + 'fe80::1027:b13d:a4a7:cd60', + '127.0.0.1', + '::1', + ], + mac: ['00:0c:29:89:ff:73', '3c:22:fb:3c:93:4c'], + name: 'DESKTOP-4I1B23J', + os: { + Ext: { variant: 'Windows 10 Pro' }, + family: 'windows', + full: 'Windows 10 Pro 2004 (10.0.19041.329)', + kernel: '2004 (10.0.19041.329)', + name: 'Windows', + platform: 'windows', + version: '2004 (10.0.19041.329)', + }, + }, + message: 'Endpoint metadata', + }, + host_status: 'error', +}; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index b56c45a9205b6..17192057d2ad3 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -48,6 +48,7 @@ import { EndpointAppContextService } from './endpoint/endpoint_app_context_servi import { EndpointAppContext } from './endpoint/types'; import { registerDownloadExceptionListRoute } from './endpoint/routes/artifacts'; import { initUsageCollectors } from './usage'; +import { AppRequestContext } from './types'; export interface SetupPlugins { alerts: AlertingSetup; @@ -127,9 +128,12 @@ export class Plugin implements IPlugin ({ - getAppClient: () => this.appClientFactory.create(request), - })); + core.http.registerRouteHandlerContext( + APP_ID, + (context, request, response): AppRequestContext => ({ + getAppClient: () => this.appClientFactory.create(request), + }) + ); this.appClientFactory.setup({ getSpaceId: plugins.spaces?.spacesService?.getSpaceId, @@ -144,7 +148,6 @@ export class Plugin implements IPlugin { const expectedHost: Omit = { _id: 'zeek-sensor-san-francisco', + endpoint: null, host: { architecture: ['x86_64'], id: [CURSOR_ID], From 0c87aa506d401b966961bb3152d78fb0e1580f0e Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Tue, 14 Jul 2020 16:18:32 -0500 Subject: [PATCH 43/82] [DOCS] Adds API keys to API docs (#71738) * [DOCS] Adds API keys to API docs * Fixes link title * Update docs/api/using-api.asciidoc Co-authored-by: Brandon Morelli Co-authored-by: Brandon Morelli --- docs/api/using-api.asciidoc | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/docs/api/using-api.asciidoc b/docs/api/using-api.asciidoc index 188c8f9a5909d..c61edfb62b079 100644 --- a/docs/api/using-api.asciidoc +++ b/docs/api/using-api.asciidoc @@ -10,7 +10,23 @@ NOTE: The {kib} Console supports only Elasticsearch APIs. You are unable to inte [float] [[api-authentication]] === Authentication -{kib} supports token-based authentication with the same username and password that you use to log into the {kib} Console. In a given HTTP tool, and when available, you can select to use its 'Basic Authentication' option, which is where the username and password are stored in order to be passed as part of the call. +The {kib} APIs support key- and token-based authentication. + +[float] +[[token-api-authentication]] +==== Token-based authentication + +To use token-based authentication, you use the same username and password that you use to log into Elastic. +In a given HTTP tool, and when available, you can select to use its 'Basic Authentication' option, +which is where the username and password are stored in order to be passed as part of the call. + +[float] +[[key-authentication]] +==== Key-based authentication + +To use key-based authentication, you create an API key using the Elastic Console, then specify the key in the header of your API calls. + +For information about API keys, refer to <>. [float] [[api-calls]] @@ -51,7 +67,8 @@ For all APIs, you must use a request header. The {kib} APIs support the `kbn-xsr * XSRF protections are disabled using the `server.xsrf.disableProtection` setting `Content-Type: application/json`:: - Applicable only when you send a payload in the API request. {kib} API requests and responses use JSON. Typically, if you include the `kbn-xsrf` header, you must also include the `Content-Type` header. + Applicable only when you send a payload in the API request. {kib} API requests and responses use JSON. + Typically, if you include the `kbn-xsrf` header, you must also include the `Content-Type` header. Request header example: From 34c54ed31b70e4b6ffaf9cec003e3878ad68583f Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 14 Jul 2020 15:19:51 -0600 Subject: [PATCH 44/82] [Maps] fix custom icon palettes UI not being displayed (#71482) * [Maps] fix custom icon palettes UI not being displayed * cleanup test * remove uneeded change to vector style defaults * fix jest tests * review feedback * fix jest tests --- .../style_property_descriptor_types.ts | 2 +- .../create_layer_descriptor.test.ts | 9 +- .../security/create_layer_descriptors.test.ts | 9 +- .../tiled_vector_layer.test.tsx | 8 +- .../sources/ems_tms_source/ems_tms_source.js | 5 +- .../vector/components/style_map_select.js | 100 ------------- .../icon_map_select.test.tsx.snap | 124 ++++++++++++++++ .../components/symbol/dynamic_icon_form.js | 5 - .../components/symbol/icon_map_select.js | 59 -------- .../symbol/icon_map_select.test.tsx | 78 ++++++++++ .../components/symbol/icon_map_select.tsx | 136 ++++++++++++++++++ .../vector/components/symbol/icon_select.js | 16 +-- .../components/symbol/icon_select.test.js | 31 ++-- .../vector/components/symbol/icon_stops.js | 38 ++--- .../components/symbol/icon_stops.test.js | 34 ++++- .../components/symbol/static_icon_form.js | 15 +- .../symbol/vector_style_icon_editor.js | 14 +- .../properties/dynamic_style_property.d.ts | 1 + .../classes/styles/vector/symbol_utils.js | 4 +- .../vector/vector_style_defaults.test.ts | 9 +- .../styles/vector/vector_style_defaults.ts | 5 +- .../plugins/maps/public/kibana_services.d.ts | 1 + x-pack/plugins/maps/public/kibana_services.js | 3 + 23 files changed, 428 insertions(+), 278 deletions(-) delete mode 100644 x-pack/plugins/maps/public/classes/styles/vector/components/style_map_select.js create mode 100644 x-pack/plugins/maps/public/classes/styles/vector/components/symbol/__snapshots__/icon_map_select.test.tsx.snap delete mode 100644 x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.js create mode 100644 x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.test.tsx create mode 100644 x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.tsx diff --git a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts index 4846054ca26cb..ce6539c9c4520 100644 --- a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts @@ -95,7 +95,7 @@ export type ColorStylePropertyDescriptor = | ColorDynamicStylePropertyDescriptor; export type IconDynamicOptions = { - iconPaletteId?: string; + iconPaletteId: string | null; customIconStops?: IconStop[]; useCustomIconMap?: boolean; field?: StylePropertyField; diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts index 075d19dccdb68..e6349fbe9ab9d 100644 --- a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts @@ -5,14 +5,9 @@ */ jest.mock('../../../../kibana_services', () => { - const mockUiSettings = { - get: () => { - return undefined; - }, - }; return { - getUiSettings: () => { - return mockUiSettings; + getIsDarkMode() { + return false; }, }; }); diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.test.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.test.ts index 49a86f45a681b..d02f07923c682 100644 --- a/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.test.ts @@ -5,14 +5,9 @@ */ jest.mock('../../../../kibana_services', () => { - const mockUiSettings = { - get: () => { - return undefined; - }, - }; return { - getUiSettings: () => { - return mockUiSettings; + getIsDarkMode() { + return false; }, }; }); diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx index ecd625db34411..faae26cac08e7 100644 --- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx +++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx @@ -8,12 +8,8 @@ import sinon from 'sinon'; jest.mock('../../../kibana_services', () => { return { - getUiSettings() { - return { - get() { - return false; - }, - }; + getIsDarkMode() { + return false; }, }; }); diff --git a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.js b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.js index 83c87eb53d4fe..b364dd32860f3 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.js +++ b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.js @@ -12,7 +12,7 @@ import { UpdateSourceEditor } from './update_source_editor'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { SOURCE_TYPES } from '../../../../common/constants'; -import { getEmsTileLayerId, getUiSettings } from '../../../kibana_services'; +import { getEmsTileLayerId, getIsDarkMode } from '../../../kibana_services'; import { registerSource } from '../source_registry'; export const sourceTitle = i18n.translate('xpack.maps.source.emsTileTitle', { @@ -122,9 +122,8 @@ export class EMSTMSSource extends AbstractTMSSource { return this._descriptor.id; } - const isDarkMode = getUiSettings().get('theme:darkMode', false); const emsTileLayerId = getEmsTileLayerId(); - return isDarkMode ? emsTileLayerId.dark : emsTileLayerId.bright; + return getIsDarkMode() ? emsTileLayerId.dark : emsTileLayerId.bright; } } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/style_map_select.js b/x-pack/plugins/maps/public/classes/styles/vector/components/style_map_select.js deleted file mode 100644 index e4dc9d1b4d8f6..0000000000000 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/style_map_select.js +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Component, Fragment } from 'react'; - -import { EuiSuperSelect, EuiSpacer } from '@elastic/eui'; - -const CUSTOM_MAP = 'CUSTOM_MAP'; - -export class StyleMapSelect extends Component { - state = {}; - - static getDerivedStateFromProps(nextProps, prevState) { - if (nextProps.customMapStops === prevState.prevPropsCustomMapStops) { - return null; - } - - return { - prevPropsCustomMapStops: nextProps.customMapStops, // reset tracker to latest value - customMapStops: nextProps.customMapStops, // reset customMapStops to latest value - }; - } - - _onMapSelect = (selectedValue) => { - const useCustomMap = selectedValue === CUSTOM_MAP; - this.props.onChange({ - selectedMapId: useCustomMap ? null : selectedValue, - useCustomMap, - }); - }; - - _onCustomMapChange = ({ customMapStops, isInvalid }) => { - // Manage invalid custom map in local state - if (isInvalid) { - this.setState({ customMapStops }); - return; - } - - this.props.onChange({ - useCustomMap: true, - customMapStops, - }); - }; - - _renderCustomStopsInput() { - return !this.props.isCustomOnly && !this.props.useCustomMap - ? null - : this.props.renderCustomStopsInput(this._onCustomMapChange); - } - - _renderMapSelect() { - if (this.props.isCustomOnly) { - return null; - } - - const mapOptionsWithCustom = [ - { - value: CUSTOM_MAP, - inputDisplay: this.props.customOptionLabel, - }, - ...this.props.options, - ]; - - let valueOfSelected; - if (this.props.useCustomMap) { - valueOfSelected = CUSTOM_MAP; - } else { - valueOfSelected = this.props.options.find( - (option) => option.value === this.props.selectedMapId - ) - ? this.props.selectedMapId - : ''; - } - - return ( - - - - - ); - } - - render() { - return ( - - {this._renderMapSelect()} - {this._renderCustomStopsInput()} - - ); - } -} diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/__snapshots__/icon_map_select.test.tsx.snap b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/__snapshots__/icon_map_select.test.tsx.snap new file mode 100644 index 0000000000000..b0b85268aa1c8 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/__snapshots__/icon_map_select.test.tsx.snap @@ -0,0 +1,124 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Should not render icon map select when isCustomOnly 1`] = ` + + + +`; + +exports[`Should render custom stops input when useCustomIconMap 1`] = ` + + + mock filledShapes option + , + "value": "filledShapes", + }, + Object { + "inputDisplay":
+ mock hollowShapes option +
, + "value": "hollowShapes", + }, + ] + } + valueOfSelected="CUSTOM_MAP_ID" + /> + + +
+`; + +exports[`Should render default props 1`] = ` + + + mock filledShapes option + , + "value": "filledShapes", + }, + Object { + "inputDisplay":
+ mock hollowShapes option +
, + "value": "hollowShapes", + }, + ] + } + valueOfSelected="filledShapes" + /> + +
+`; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js index e3724d42a783b..0601922077b4a 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js @@ -12,11 +12,9 @@ import { IconMapSelect } from './icon_map_select'; export function DynamicIconForm({ fields, - isDarkMode, onDynamicStyleChange, staticDynamicSelect, styleProperty, - symbolOptions, }) { const styleOptions = styleProperty.getOptions(); @@ -44,11 +42,8 @@ export function DynamicIconForm({ return ( ); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.js deleted file mode 100644 index 6cfe656d65a1e..0000000000000 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.js +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; - -import { StyleMapSelect } from '../style_map_select'; -import { i18n } from '@kbn/i18n'; -import { IconStops } from './icon_stops'; -import { getIconPaletteOptions } from '../../symbol_utils'; - -export function IconMapSelect({ - customIconStops, - iconPaletteId, - isDarkMode, - onChange, - styleProperty, - symbolOptions, - useCustomIconMap, - isCustomOnly, -}) { - function onMapSelectChange({ customMapStops, selectedMapId, useCustomMap }) { - onChange({ - customIconStops: customMapStops, - iconPaletteId: selectedMapId, - useCustomIconMap: useCustomMap, - }); - } - - function renderCustomIconStopsInput(onCustomMapChange) { - return ( - - ); - } - - return ( - - ); -} diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.test.tsx new file mode 100644 index 0000000000000..4e68baf0bd7b7 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.test.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable max-classes-per-file */ + +jest.mock('./icon_stops', () => ({ + IconStops: () => { + return
mockIconStops
; + }, +})); + +jest.mock('../../symbol_utils', () => { + return { + getIconPaletteOptions: () => { + return [ + { value: 'filledShapes', inputDisplay:
mock filledShapes option
}, + { value: 'hollowShapes', inputDisplay:
mock hollowShapes option
}, + ]; + }, + PREFERRED_ICONS: ['circle'], + }; +}); + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { FIELD_ORIGIN } from '../../../../../../common/constants'; +import { AbstractField } from '../../../../fields/field'; +import { IDynamicStyleProperty } from '../../properties/dynamic_style_property'; +import { IconMapSelect } from './icon_map_select'; + +class MockField extends AbstractField {} + +class MockDynamicStyleProperty { + getField() { + return new MockField({ fieldName: 'myField', origin: FIELD_ORIGIN.SOURCE }); + } + + getValueSuggestions() { + return []; + } +} + +const defaultProps = { + iconPaletteId: 'filledShapes', + onChange: () => {}, + styleProperty: (new MockDynamicStyleProperty() as unknown) as IDynamicStyleProperty, + isCustomOnly: false, +}; + +test('Should render default props', () => { + const component = shallow(); + + expect(component).toMatchSnapshot(); +}); + +test('Should render custom stops input when useCustomIconMap', () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); +}); + +test('Should not render icon map select when isCustomOnly', () => { + const component = shallow(); + + expect(component).toMatchSnapshot(); +}); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.tsx new file mode 100644 index 0000000000000..1dd55bbb47f78 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.tsx @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component, Fragment } from 'react'; +import { EuiSuperSelect, EuiSpacer } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +// @ts-expect-error +import { IconStops } from './icon_stops'; +// @ts-expect-error +import { getIconPaletteOptions, PREFERRED_ICONS } from '../../symbol_utils'; +import { IconStop } from '../../../../../../common/descriptor_types'; +import { IDynamicStyleProperty } from '../../properties/dynamic_style_property'; + +const CUSTOM_MAP_ID = 'CUSTOM_MAP_ID'; + +const DEFAULT_ICON_STOPS = [ + { stop: null, icon: PREFERRED_ICONS[0] }, // first stop is the "other" category + { stop: '', icon: PREFERRED_ICONS[1] }, +]; + +interface StyleOptionChanges { + customIconStops?: IconStop[]; + iconPaletteId?: string | null; + useCustomIconMap: boolean; +} + +interface Props { + customIconStops?: IconStop[]; + iconPaletteId: string | null; + onChange: ({ customIconStops, iconPaletteId, useCustomIconMap }: StyleOptionChanges) => void; + styleProperty: IDynamicStyleProperty; + useCustomIconMap?: boolean; + isCustomOnly: boolean; +} + +interface State { + customIconStops: IconStop[]; +} + +export class IconMapSelect extends Component { + state = { + customIconStops: this.props.customIconStops ? this.props.customIconStops : DEFAULT_ICON_STOPS, + }; + + _onMapSelect = (selectedValue: string) => { + const useCustomIconMap = selectedValue === CUSTOM_MAP_ID; + const changes: StyleOptionChanges = { + iconPaletteId: useCustomIconMap ? null : selectedValue, + useCustomIconMap, + }; + // edge case when custom palette is first enabled + // customIconStops is undefined so need to update custom stops with default so icons are rendered. + if (!this.props.customIconStops) { + changes.customIconStops = DEFAULT_ICON_STOPS; + } + this.props.onChange(changes); + }; + + _onCustomMapChange = ({ + customStops, + isInvalid, + }: { + customStops: IconStop[]; + isInvalid: boolean; + }) => { + // Manage invalid custom map in local state + this.setState({ customIconStops: customStops }); + + if (!isInvalid) { + this.props.onChange({ + useCustomIconMap: true, + customIconStops: customStops, + }); + } + }; + + _renderCustomStopsInput() { + return !this.props.isCustomOnly && !this.props.useCustomIconMap ? null : ( + + ); + } + + _renderMapSelect() { + if (this.props.isCustomOnly) { + return null; + } + + const mapOptionsWithCustom = [ + { + value: CUSTOM_MAP_ID, + inputDisplay: i18n.translate('xpack.maps.styles.icon.customMapLabel', { + defaultMessage: 'Custom icon palette', + }), + }, + ...getIconPaletteOptions(), + ]; + + let valueOfSelected = ''; + if (this.props.useCustomIconMap) { + valueOfSelected = CUSTOM_MAP_ID; + } else if (this.props.iconPaletteId) { + valueOfSelected = this.props.iconPaletteId; + } + + return ( + + + + + ); + } + + render() { + return ( + + {this._renderMapSelect()} + {this._renderCustomStopsInput()} + + ); + } +} diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.js index 1ceff3e3ba801..c8ad869d33d33 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.js @@ -15,6 +15,8 @@ import { EuiSelectable, } from '@elastic/eui'; import { SymbolIcon } from '../legend/symbol_icon'; +import { SYMBOL_OPTIONS } from '../../symbol_utils'; +import { getIsDarkMode } from '../../../../../kibana_services'; function isKeyboardEvent(event) { return typeof event === 'object' && 'keyCode' in event; @@ -62,7 +64,6 @@ export class IconSelect extends Component { }; _renderPopoverButton() { - const { isDarkMode, value } = this.props; return ( } /> @@ -93,8 +94,7 @@ export class IconSelect extends Component { } _renderIconSelectable() { - const { isDarkMode } = this.props; - const options = this.props.symbolOptions.map(({ value, label }) => { + const options = SYMBOL_OPTIONS.map(({ value, label }) => { return { value, label, @@ -102,7 +102,7 @@ export class IconSelect extends Component { ), }; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.test.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.test.js index 56dce6fad8386..8dc2057054e62 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.test.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.test.js @@ -4,25 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ +jest.mock('../../../../../kibana_services', () => { + return { + getIsDarkMode() { + return false; + }, + }; +}); + +jest.mock('../../symbol_utils', () => { + return { + SYMBOL_OPTIONS: [ + { value: 'symbol1', label: 'symbol1' }, + { value: 'symbol2', label: 'symbol2' }, + ], + }; +}); + import React from 'react'; import { shallow } from 'enzyme'; import { IconSelect } from './icon_select'; -const symbolOptions = [ - { value: 'symbol1', label: 'symbol1' }, - { value: 'symbol2', label: 'symbol2' }, -]; - test('Should render icon select', () => { - const component = shallow( - {}} - symbolOptions={symbolOptions} - isDarkMode={false} - /> - ); + const component = shallow( {}} />); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_stops.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_stops.js index 81a44fcaadbd3..78fa6c10b899d 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_stops.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_stops.js @@ -11,7 +11,7 @@ import { getOtherCategoryLabel } from '../../style_util'; import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiFieldText } from '@elastic/eui'; import { IconSelect } from './icon_select'; import { StopInput } from '../stop_input'; -import { PREFERRED_ICONS } from '../../symbol_utils'; +import { PREFERRED_ICONS, SYMBOL_OPTIONS } from '../../symbol_utils'; function isDuplicateStop(targetStop, iconStops) { const stops = iconStops.filter(({ stop }) => { @@ -20,7 +20,7 @@ function isDuplicateStop(targetStop, iconStops) { return stops.length > 1; } -export function getFirstUnusedSymbol(symbolOptions, iconStops) { +export function getFirstUnusedSymbol(iconStops) { const firstUnusedPreferredIconId = PREFERRED_ICONS.find((iconId) => { const isSymbolBeingUsed = iconStops.some(({ icon }) => { return icon === iconId; @@ -32,7 +32,7 @@ export function getFirstUnusedSymbol(symbolOptions, iconStops) { return firstUnusedPreferredIconId; } - const firstUnusedSymbol = symbolOptions.find(({ value }) => { + const firstUnusedSymbol = SYMBOL_OPTIONS.find(({ value }) => { const isSymbolBeingUsed = iconStops.some(({ icon }) => { return icon === value; }); @@ -42,19 +42,7 @@ export function getFirstUnusedSymbol(symbolOptions, iconStops) { return firstUnusedSymbol ? firstUnusedSymbol.value : DEFAULT_ICON; } -const DEFAULT_ICON_STOPS = [ - { stop: null, icon: PREFERRED_ICONS[0] }, //first stop is the "other" color - { stop: '', icon: PREFERRED_ICONS[1] }, -]; - -export function IconStops({ - field, - getValueSuggestions, - iconStops = DEFAULT_ICON_STOPS, - isDarkMode, - onChange, - symbolOptions, -}) { +export function IconStops({ field, getValueSuggestions, iconStops, onChange }) { return iconStops.map(({ stop, icon }, index) => { const onIconSelect = (selectedIconId) => { const newIconStops = [...iconStops]; @@ -62,7 +50,7 @@ export function IconStops({ ...iconStops[index], icon: selectedIconId, }; - onChange({ customMapStops: newIconStops }); + onChange({ customStops: newIconStops }); }; const onStopChange = (newStopValue) => { const newIconStops = [...iconStops]; @@ -71,17 +59,17 @@ export function IconStops({ stop: newStopValue, }; onChange({ - customMapStops: newIconStops, + customStops: newIconStops, isInvalid: isDuplicateStop(newStopValue, iconStops), }); }; const onAdd = () => { onChange({ - customMapStops: [ + customStops: [ ...iconStops.slice(0, index + 1), { stop: '', - icon: getFirstUnusedSymbol(symbolOptions, iconStops), + icon: getFirstUnusedSymbol(iconStops), }, ...iconStops.slice(index + 1), ], @@ -89,7 +77,7 @@ export function IconStops({ }; const onRemove = () => { onChange({ - customMapStops: [...iconStops.slice(0, index), ...iconStops.slice(index + 1)], + customStops: [...iconStops.slice(0, index), ...iconStops.slice(index + 1)], }); }; @@ -157,13 +145,7 @@ export function IconStops({ {stopInput} - +
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_stops.test.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_stops.test.js index ffe9b6feef462..fe73659b0fe58 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_stops.test.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_stops.test.js @@ -4,17 +4,41 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; import { getFirstUnusedSymbol } from './icon_stops'; -describe('getFirstUnusedSymbol', () => { - const symbolOptions = [{ value: 'icon1' }, { value: 'icon2' }]; +jest.mock('./icon_select', () => ({ + IconSelect: () => { + return
mockIconSelect
; + }, +})); + +jest.mock('../../symbol_utils', () => { + return { + SYMBOL_OPTIONS: [{ value: 'icon1' }, { value: 'icon2' }], + PREFERRED_ICONS: [ + 'circle', + 'marker', + 'square', + 'star', + 'triangle', + 'hospital', + 'circle-stroked', + 'marker-stroked', + 'square-stroked', + 'star-stroked', + 'triangle-stroked', + ], + }; +}); +describe('getFirstUnusedSymbol', () => { test('Should return first unused icon from PREFERRED_ICONS', () => { const iconStops = [ { stop: 'category1', icon: 'circle' }, { stop: 'category2', icon: 'marker' }, ]; - const nextIcon = getFirstUnusedSymbol(symbolOptions, iconStops); + const nextIcon = getFirstUnusedSymbol(iconStops); expect(nextIcon).toBe('square'); }); @@ -33,7 +57,7 @@ describe('getFirstUnusedSymbol', () => { { stop: 'category11', icon: 'triangle-stroked' }, { stop: 'category12', icon: 'icon1' }, ]; - const nextIcon = getFirstUnusedSymbol(symbolOptions, iconStops); + const nextIcon = getFirstUnusedSymbol(iconStops); expect(nextIcon).toBe('icon2'); }); @@ -53,7 +77,7 @@ describe('getFirstUnusedSymbol', () => { { stop: 'category12', icon: 'icon1' }, { stop: 'category13', icon: 'icon2' }, ]; - const nextIcon = getFirstUnusedSymbol(symbolOptions, iconStops); + const nextIcon = getFirstUnusedSymbol(iconStops); expect(nextIcon).toBe('marker'); }); }); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/static_icon_form.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/static_icon_form.js index 56e5737f72449..986f279dddc1a 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/static_icon_form.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/static_icon_form.js @@ -8,13 +8,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { IconSelect } from './icon_select'; -export function StaticIconForm({ - isDarkMode, - onStaticStyleChange, - staticDynamicSelect, - styleProperty, - symbolOptions, -}) { +export function StaticIconForm({ onStaticStyleChange, staticDynamicSelect, styleProperty }) { const onChange = (selectedIconId) => { onStaticStyleChange(styleProperty.getStyleName(), { value: selectedIconId }); }; @@ -25,12 +19,7 @@ export function StaticIconForm({ {staticDynamicSelect} - + ); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/vector_style_icon_editor.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/vector_style_icon_editor.js index 36b6c1a76470c..2a983a32f0d82 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/vector_style_icon_editor.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/vector_style_icon_editor.js @@ -6,25 +6,15 @@ import React from 'react'; -import { getUiSettings } from '../../../../../kibana_services'; import { StylePropEditor } from '../style_prop_editor'; import { DynamicIconForm } from './dynamic_icon_form'; import { StaticIconForm } from './static_icon_form'; -import { SYMBOL_OPTIONS } from '../../symbol_utils'; export function VectorStyleIconEditor(props) { const iconForm = props.styleProperty.isDynamic() ? ( - + ) : ( - + ); return {iconForm}; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.d.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.d.ts index b53623ab52edb..e153b6e4850f7 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.d.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.d.ts @@ -33,4 +33,5 @@ export interface IDynamicStyleProperty extends IStyleProperty { pluckCategoricalStyleMetaFromFeatures(features: unknown[]): CategoryFieldMeta; pluckOrdinalStyleMetaFromFieldMetaData(fieldMetaData: unknown): RangeFieldMeta; pluckCategoricalStyleMetaFromFieldMetaData(fieldMetaData: unknown): CategoryFieldMeta; + getValueSuggestions(query: string): string[]; } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js b/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js index 04df9d73d75cd..3a5f9b8f6690e 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js @@ -9,6 +9,7 @@ import maki from '@elastic/maki'; import xml2js from 'xml2js'; import { parseXmlString } from '../../../../common/parse_xml_string'; import { SymbolIcon } from './components/legend/symbol_icon'; +import { getIsDarkMode } from '../../../kibana_services'; export const LARGE_MAKI_ICON_SIZE = 15; const LARGE_MAKI_ICON_SIZE_AS_STRING = LARGE_MAKI_ICON_SIZE.toString(); @@ -111,7 +112,8 @@ ICON_PALETTES.forEach((iconPalette) => { }); }); -export function getIconPaletteOptions(isDarkMode) { +export function getIconPaletteOptions() { + const isDarkMode = getIsDarkMode(); return ICON_PALETTES.map(({ id, icons }) => { const iconsDisplay = icons.map((iconId) => { const style = { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.test.ts b/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.test.ts index bc032639dd07d..d630d2909b3d8 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.test.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.test.ts @@ -5,14 +5,9 @@ */ jest.mock('../../../kibana_services', () => { - const mockUiSettings = { - get: () => { - return undefined; - }, - }; return { - getUiSettings: () => { - return mockUiSettings; + getIsDarkMode() { + return false; }, }; }); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts b/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts index a3ae80e0a5935..50321510c2ba8 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts @@ -18,8 +18,7 @@ import { CATEGORICAL_COLOR_PALETTES, } from '../color_palettes'; import { VectorStylePropertiesDescriptor } from '../../../../common/descriptor_types'; -// @ts-ignore -import { getUiSettings } from '../../../kibana_services'; +import { getIsDarkMode } from '../../../kibana_services'; export const MIN_SIZE = 1; export const MAX_SIZE = 64; @@ -67,7 +66,7 @@ export function getDefaultStaticProperties( const nextFillColor = DEFAULT_FILL_COLORS[nextColorIndex]; const nextLineColor = DEFAULT_LINE_COLORS[nextColorIndex]; - const isDarkMode = getUiSettings().get('theme:darkMode', false); + const isDarkMode = getIsDarkMode(); return { [VECTOR_STYLES.ICON]: { diff --git a/x-pack/plugins/maps/public/kibana_services.d.ts b/x-pack/plugins/maps/public/kibana_services.d.ts index 8fa52500fb16e..d4a7fa5d50af8 100644 --- a/x-pack/plugins/maps/public/kibana_services.d.ts +++ b/x-pack/plugins/maps/public/kibana_services.d.ts @@ -24,6 +24,7 @@ export function getVisualizations(): any; export function getDocLinks(): any; export function getCoreChrome(): any; export function getUiSettings(): any; +export function getIsDarkMode(): boolean; export function getCoreOverlays(): any; export function getData(): any; export function getUiActions(): any; diff --git a/x-pack/plugins/maps/public/kibana_services.js b/x-pack/plugins/maps/public/kibana_services.js index 1684acfb0f463..97d7f0c66c629 100644 --- a/x-pack/plugins/maps/public/kibana_services.js +++ b/x-pack/plugins/maps/public/kibana_services.js @@ -40,6 +40,9 @@ export const getFileUploadComponent = () => { let uiSettings; export const setUiSettings = (coreUiSettings) => (uiSettings = coreUiSettings); export const getUiSettings = () => uiSettings; +export const getIsDarkMode = () => { + return getUiSettings().get('theme:darkMode', false); +}; let indexPatternSelectComponent; export const setIndexPatternSelect = (indexPatternSelect) => From 9506dc90caafd4b4ecbee6dd29dbca3d5418654c Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Tue, 14 Jul 2020 16:25:31 -0500 Subject: [PATCH 45/82] [DOCS] Adds ID to logstash pipeline (#71726) --- .../logstash-configuration-management/create-logstash.asciidoc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/api/logstash-configuration-management/create-logstash.asciidoc b/docs/api/logstash-configuration-management/create-logstash.asciidoc index 9bd5a9028ee9a..b608f4ee698f7 100644 --- a/docs/api/logstash-configuration-management/create-logstash.asciidoc +++ b/docs/api/logstash-configuration-management/create-logstash.asciidoc @@ -20,6 +20,9 @@ experimental[] Create a centrally-managed Logstash pipeline, or update an existi [[logstash-configuration-management-api-create-request-body]] ==== Request body +`id`:: + (Required, string) The pipeline ID. + `description`:: (Optional, string) The pipeline description. From 754ade5130a18604c0a1d5bb01e8442568c8dd44 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 15 Jul 2020 00:26:39 +0300 Subject: [PATCH 46/82] [SIEM] Fix custom date time mapping bug (#70713) Co-authored-by: Xavier Mouligneau Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Co-authored-by: Elastic Machine --- .../common/graphql/shared/schema.gql.ts | 9 +- .../common/types/timeline/index.ts | 8 +- .../integration/ml_conditional_links.spec.ts | 26 +-- .../integration/url_compatibility.spec.ts | 22 +- .../cypress/integration/url_state.spec.ts | 68 +++--- .../security_solution/cypress/urls/state.ts | 18 +- .../components/alerts_viewer/alerts_table.tsx | 8 +- .../events_viewer/events_viewer.test.tsx | 112 ++++++++- .../events_viewer/events_viewer.tsx | 30 ++- .../components/events_viewer/index.test.tsx | 6 +- .../common/components/events_viewer/index.tsx | 12 +- .../common/components/events_viewer/mock.ts | 12 +- .../matrix_histogram/index.test.tsx | 4 +- .../components/matrix_histogram/index.tsx | 4 +- .../components/matrix_histogram/types.ts | 12 +- .../components/matrix_histogram/utils.test.ts | 8 +- .../components/matrix_histogram/utils.ts | 4 +- .../ml/anomaly/anomaly_table_provider.tsx | 4 +- .../ml/anomaly/use_anomalies_table_data.ts | 10 +- .../ml/links/create_explorer_link.test.ts | 4 +- .../ml/links/create_explorer_link.tsx | 6 +- .../__snapshots__/anomaly_score.test.tsx.snap | 4 +- .../anomaly_scores.test.tsx.snap | 8 +- .../create_descriptions_list.test.tsx.snap | 4 +- .../ml/score/anomaly_score.test.tsx | 10 +- .../components/ml/score/anomaly_score.tsx | 4 +- .../ml/score/anomaly_scores.test.tsx | 17 +- .../components/ml/score/anomaly_scores.tsx | 4 +- .../ml/score/create_description_list.tsx | 4 +- .../score/create_descriptions_list.test.tsx | 11 +- .../score/score_interval_to_datetime.test.ts | 16 +- .../ml/score/score_interval_to_datetime.ts | 12 +- .../get_anomalies_host_table_columns.test.tsx | 4 +- .../get_anomalies_host_table_columns.tsx | 8 +- ...t_anomalies_network_table_columns.test.tsx | 4 +- .../get_anomalies_network_table_columns.tsx | 8 +- .../ml/tables/host_equality.test.ts | 48 ++-- .../ml/tables/network_equality.test.ts | 56 ++--- .../public/common/components/ml/types.ts | 4 +- .../navigation/breadcrumbs/index.test.ts | 30 +-- .../components/navigation/index.test.tsx | 24 +- .../navigation/tab_navigation/index.test.tsx | 16 +- .../components/stat_items/index.test.tsx | 16 +- .../common/components/stat_items/index.tsx | 8 +- .../super_date_picker/index.test.tsx | 8 +- .../components/super_date_picker/index.tsx | 4 +- .../super_date_picker/selectors.test.ts | 28 +-- .../common/components/top_n/index.test.tsx | 14 +- .../common/components/top_n/top_n.test.tsx | 16 +- .../public/common/components/top_n/top_n.tsx | 4 +- .../__mocks__/normalize_time_range.ts | 10 + .../components/url_state/index.test.tsx | 29 +-- .../url_state/index_mocked.test.tsx | 20 +- .../url_state/initialize_redux_by_url.tsx | 5 + .../url_state/normalize_time_range.test.ts | 132 +++++------ .../url_state/normalize_time_range.ts | 13 +- .../components/url_state/test_dependencies.ts | 8 +- .../public/common/components/utils.ts | 2 +- .../events/last_event_time/index.ts | 4 + .../last_event_time.gql_query.ts | 8 +- .../containers/events/last_event_time/mock.ts | 1 + .../common/containers/global_time/index.tsx | 98 ++++++++ .../matrix_histogram/index.test.tsx | 12 +- .../common/containers/query_template.tsx | 8 +- .../containers/query_template_paginated.tsx | 8 +- .../common/containers/source/index.test.tsx | 11 + .../public/common/containers/source/index.tsx | 34 +++ .../public/common/containers/source/mock.ts | 13 +- .../public/common/mock/global_state.ts | 20 +- .../public/common/mock/timeline_results.ts | 12 +- .../public/common/store/inputs/actions.ts | 12 +- .../common/store/inputs/helpers.test.ts | 24 +- .../public/common/store/inputs/model.ts | 13 +- .../utils/default_date_settings.test.ts | 36 +-- .../common/utils/default_date_settings.ts | 4 +- .../alerts_histogram.test.tsx | 4 +- .../alerts_histogram.tsx | 4 +- .../alerts_histogram_panel/helpers.tsx | 7 +- .../alerts_histogram_panel/index.test.tsx | 4 +- .../components/alerts_table/actions.test.tsx | 16 +- .../components/alerts_table/actions.tsx | 4 +- .../components/alerts_table/index.test.tsx | 4 +- .../components/alerts_table/index.tsx | 4 +- .../components/alerts_table/types.ts | 4 +- .../rules/fetch_index_patterns.test.tsx | 11 + .../rules/fetch_index_patterns.tsx | 53 +++-- .../detection_engine.test.tsx | 9 +- .../detection_engine/detection_engine.tsx | 6 +- .../rules/details/index.test.tsx | 9 +- .../detection_engine/rules/details/index.tsx | 6 +- .../public/graphql/introspection.json | 219 +++++++++++++++++- .../security_solution/public/graphql/types.ts | 50 +++- .../hosts/components/kpi_hosts/index.test.tsx | 4 +- .../hosts/components/kpi_hosts/index.tsx | 4 +- .../authentications/index.gql_query.ts | 2 + .../containers/authentications/index.tsx | 2 + .../first_last_seen.gql_query.ts | 13 +- .../containers/hosts/first_last_seen/index.ts | 4 +- .../containers/hosts/first_last_seen/mock.ts | 1 + .../containers/hosts/hosts_table.gql_query.ts | 2 + .../public/hosts/containers/hosts/index.tsx | 10 +- .../hosts/containers/hosts/overview/index.tsx | 8 +- .../hosts/pages/details/details_tabs.test.tsx | 19 +- .../hosts/pages/details/details_tabs.tsx | 9 +- .../public/hosts/pages/details/index.tsx | 9 +- .../public/hosts/pages/details/types.ts | 10 +- .../public/hosts/pages/hosts.tsx | 9 +- .../public/hosts/pages/hosts_tabs.tsx | 11 +- .../authentications_query_tab_body.tsx | 2 + .../pages/navigation/hosts_query_tab_body.tsx | 2 + .../public/hosts/pages/navigation/types.ts | 2 + .../public/hosts/pages/types.ts | 6 +- .../embeddables/embedded_map.test.tsx | 4 +- .../components/embeddables/embedded_map.tsx | 4 +- .../embeddables/embedded_map_helpers.test.tsx | 8 +- .../__snapshots__/index.test.tsx.snap | 4 +- .../components/ip_overview/index.test.tsx | 4 +- .../network/components/ip_overview/index.tsx | 4 +- .../__snapshots__/index.test.tsx.snap | 8 +- .../components/kpi_network/index.test.tsx | 4 +- .../network/components/kpi_network/index.tsx | 8 +- .../network/components/kpi_network/mock.ts | 4 +- .../containers/ip_overview/index.gql_query.ts | 8 +- .../network/containers/ip_overview/index.tsx | 3 +- .../public/network/containers/tls/index.tsx | 4 +- .../network/pages/ip_details/index.test.tsx | 17 +- .../public/network/pages/ip_details/index.tsx | 3 +- .../public/network/pages/ip_details/types.ts | 4 +- .../pages/navigation/network_routes.tsx | 6 +- .../public/network/pages/navigation/types.ts | 4 +- .../public/network/pages/network.test.tsx | 4 +- .../public/network/pages/network.tsx | 6 +- .../public/network/pages/types.ts | 4 +- .../alerts_by_category/index.test.tsx | 4 +- .../components/event_counts/index.test.tsx | 4 +- .../__snapshots__/index.test.tsx.snap | 4 +- .../components/host_overview/index.test.tsx | 4 +- .../components/host_overview/index.tsx | 4 +- .../components/overview_host/index.test.tsx | 4 +- .../overview_network/index.test.tsx | 4 +- .../components/signals_by_category/index.tsx | 6 +- .../containers/overview_host/index.tsx | 4 +- .../containers/overview_network/index.tsx | 4 +- .../public/overview/pages/overview.test.tsx | 9 +- .../open_timeline/export_timeline/mocks.ts | 2 +- .../components/open_timeline/helpers.test.ts | 57 ++--- .../components/open_timeline/helpers.ts | 11 +- .../components/open_timeline/types.ts | 4 +- .../__snapshots__/timeline.test.tsx.snap | 6 +- .../components/timeline/body/events/index.tsx | 5 +- .../timeline/body/events/stateful_event.tsx | 5 +- .../components/timeline/body/index.test.tsx | 1 + .../components/timeline/body/index.tsx | 5 +- .../timeline/body/stateful_body.tsx | 6 +- .../components/timeline/helpers.test.tsx | 45 ++-- .../timelines/components/timeline/helpers.tsx | 19 +- .../components/timeline/index.test.tsx | 6 +- .../timelines/components/timeline/index.tsx | 7 +- .../timeline/query_bar/index.test.tsx | 24 +- .../components/timeline/query_bar/index.tsx | 4 +- .../search_or_filter/search_or_filter.tsx | 4 +- .../components/timeline/timeline.test.tsx | 42 +++- .../components/timeline/timeline.tsx | 70 ++++-- .../containers/details/index.gql_query.ts | 8 +- .../timelines/containers/details/index.tsx | 4 + .../timelines/containers/index.gql_query.ts | 4 + .../public/timelines/containers/index.tsx | 11 + .../timelines/store/timeline/actions.ts | 6 +- .../timelines/store/timeline/defaults.ts | 9 +- .../timelines/store/timeline/epic.test.ts | 10 +- .../timeline/epic_local_storage.test.tsx | 6 +- .../timelines/store/timeline/helpers.ts | 13 +- .../public/timelines/store/timeline/model.ts | 4 +- .../timelines/store/timeline/reducer.test.ts | 54 ++--- .../graphql/authentications/schema.gql.ts | 1 + .../server/graphql/events/resolvers.ts | 1 + .../server/graphql/events/schema.gql.ts | 3 + .../server/graphql/hosts/resolvers.ts | 1 + .../server/graphql/hosts/schema.gql.ts | 8 +- .../server/graphql/ip_details/schema.gql.ts | 1 + .../server/graphql/network/schema.gql.ts | 1 + .../server/graphql/timeline/schema.gql.ts | 4 +- .../security_solution/server/graphql/types.ts | 58 ++++- .../server/lib/authentications/query.dsl.ts | 5 + .../lib/events/elasticsearch_adapter.ts | 2 +- .../server/lib/events/query.dsl.ts | 74 +----- .../lib/events/query.last_event_time.dsl.ts | 6 + .../server/lib/events/types.ts | 8 +- .../server/lib/framework/types.ts | 2 + .../server/lib/hosts/mock.ts | 4 +- .../server/lib/hosts/query.hosts.dsl.ts | 5 + .../hosts/query.last_first_seen_host.dsl.ts | 3 + .../server/lib/hosts/types.ts | 2 + .../lib/ip_details/query_overview.dsl.ts | 9 +- .../server/lib/ip_details/query_users.dsl.ts | 6 +- .../server/lib/kpi_hosts/mock.ts | 4 +- .../lib/kpi_hosts/query_authentication.dsl.ts | 1 + .../server/lib/kpi_hosts/query_hosts.dsl.ts | 1 + .../lib/kpi_hosts/query_unique_ips.dsl.ts | 1 + .../server/lib/kpi_network/mock.ts | 8 +- .../server/lib/kpi_network/query_dns.dsl.ts | 1 + .../lib/kpi_network/query_network_events.ts | 1 + .../kpi_network/query_tls_handshakes.dsl.ts | 1 + .../lib/kpi_network/query_unique_flow.ts | 1 + .../query_unique_private_ips.dsl.ts | 1 + .../query.anomalies_over_time.dsl.ts | 7 +- .../query.authentications_over_time.dsl.ts | 7 +- .../query.events_over_time.dsl.ts | 7 +- .../lib/matrix_histogram/query_alerts.dsl.ts | 7 +- .../query_dns_histogram.dsl.ts | 1 + .../server/lib/network/mock.ts | 2 +- .../server/lib/network/query_dns.dsl.ts | 5 + .../server/lib/network/query_http.dsl.ts | 6 +- .../lib/network/query_top_countries.dsl.ts | 6 +- .../lib/network/query_top_n_flow.dsl.ts | 6 +- .../server/lib/overview/mock.ts | 16 +- .../server/lib/overview/query.dsl.ts | 2 + .../routes/__mocks__/import_timelines.ts | 10 +- .../routes/__mocks__/request_responses.ts | 6 +- .../security_solution/server/lib/tls/mock.ts | 2 +- .../server/lib/tls/query_tls.dsl.ts | 6 +- .../lib/uncommon_processes/query.dsl.ts | 1 + .../calculate_timeseries_interval.ts | 4 +- .../utils/build_query/create_options.test.ts | 73 +++++- .../utils/build_query/create_options.ts | 5 + .../apis/security_solution/authentications.ts | 6 +- .../apis/security_solution/hosts.ts | 8 +- .../apis/security_solution/ip_overview.ts | 2 + .../security_solution/kpi_host_details.ts | 6 +- .../apis/security_solution/kpi_hosts.ts | 10 +- .../apis/security_solution/kpi_network.ts | 10 +- .../apis/security_solution/network_dns.ts | 6 +- .../security_solution/network_top_n_flow.ts | 8 +- .../apis/security_solution/overview_host.ts | 5 +- .../security_solution/overview_network.ts | 15 +- .../saved_objects/timeline.ts | 2 +- .../apis/security_solution/sources.ts | 1 + .../apis/security_solution/timeline.ts | 20 +- .../security_solution/timeline_details.ts | 1 + .../apis/security_solution/tls.ts | 8 +- .../security_solution/uncommon_processes.ts | 8 +- .../apis/security_solution/users.ts | 5 +- 242 files changed, 2024 insertions(+), 979 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/url_state/__mocks__/normalize_time_range.ts create mode 100644 x-pack/plugins/security_solution/public/common/containers/global_time/index.tsx diff --git a/x-pack/plugins/security_solution/common/graphql/shared/schema.gql.ts b/x-pack/plugins/security_solution/common/graphql/shared/schema.gql.ts index d043c1587d3c3..546fdd68b4257 100644 --- a/x-pack/plugins/security_solution/common/graphql/shared/schema.gql.ts +++ b/x-pack/plugins/security_solution/common/graphql/shared/schema.gql.ts @@ -11,9 +11,14 @@ export const sharedSchema = gql` "The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan." interval: String! "The end of the timerange" - to: Float! + to: String! "The beginning of the timerange" - from: Float! + from: String! + } + + input docValueFieldsInput { + field: String! + format: String! } type CursorType { diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 021e5a7f00b17..98d17fc87f6ce 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -124,8 +124,12 @@ const SavedFilterQueryQueryRuntimeType = runtimeTypes.partial({ * DatePicker Range Types */ const SavedDateRangePickerRuntimeType = runtimeTypes.partial({ - start: unionWithNullType(runtimeTypes.number), - end: unionWithNullType(runtimeTypes.number), + /* Before the change of all timestamp to ISO string the values of start and from + * attributes where a number. Specifically UNIX timestamps. + * To support old timeline's saved object we need to add the number io-ts type + */ + start: unionWithNullType(runtimeTypes.union([runtimeTypes.string, runtimeTypes.number])), + end: unionWithNullType(runtimeTypes.union([runtimeTypes.string, runtimeTypes.number])), }); /* diff --git a/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts b/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts index 6b3fc9e751ea4..0b302efd655a8 100644 --- a/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts @@ -94,7 +94,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkSingleIpNullKqlQuery); cy.url().should( 'include', - '/app/security/network/ip/127.0.0.1/source?timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' + '/app/security/network/ip/127.0.0.1/source?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))' ); }); @@ -102,7 +102,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkSingleIpKqlQuery); cy.url().should( 'include', - '/app/security/network/ip/127.0.0.1/source?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' + '/app/security/network/ip/127.0.0.1/source?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))' ); }); @@ -110,7 +110,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpNullKqlQuery); cy.url().should( 'include', - 'app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999))' + 'app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27))' ); }); @@ -118,7 +118,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpKqlQuery); cy.url().should( 'include', - '/app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' + '/app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))' ); }); @@ -126,7 +126,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkNullKqlQuery); cy.url().should( 'include', - '/app/security/network/flows?timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' + '/app/security/network/flows?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))' ); }); @@ -134,7 +134,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkKqlQuery); cy.url().should( 'include', - '/app/security/network/flows?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' + '/app/security/network/flows?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))' ); }); @@ -142,7 +142,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostSingleHostNullKqlQuery); cy.url().should( 'include', - '/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' + '/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' ); }); @@ -150,7 +150,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostSingleHostKqlQueryVariable); cy.url().should( 'include', - '/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' + '/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' ); }); @@ -158,7 +158,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostSingleHostKqlQuery); cy.url().should( 'include', - '/app/security/hosts/siem-windows/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' + '/app/security/hosts/siem-windows/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' ); }); @@ -166,7 +166,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostMultiHostNullKqlQuery); cy.url().should( 'include', - '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' + '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' ); }); @@ -174,7 +174,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostMultiHostKqlQuery); cy.url().should( 'include', - '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' + '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' ); }); @@ -182,7 +182,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostVariableHostNullKqlQuery); cy.url().should( 'include', - '/app/security/hosts/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' + '/app/security/hosts/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' ); }); @@ -190,7 +190,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostVariableHostKqlQuery); cy.url().should( 'include', - '/app/security/hosts/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' + '/app/security/hosts/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' ); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts index 205a49fc771cf..5b42897b065e3 100644 --- a/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts @@ -4,9 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { loginAndWaitForPage } from '../tasks/login'; +import { loginAndWaitForPage, loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { DETECTIONS } from '../urls/navigation'; +import { ABSOLUTE_DATE_RANGE } from '../urls/state'; +import { + DATE_PICKER_START_DATE_POPOVER_BUTTON, + DATE_PICKER_END_DATE_POPOVER_BUTTON, +} from '../screens/date_picker'; + +const ABSOLUTE_DATE = { + endTime: '2019-08-01T20:33:29.186Z', + startTime: '2019-08-01T20:03:29.186Z', +}; describe('URL compatibility', () => { it('Redirects to Detection alerts from old Detections URL', () => { @@ -14,4 +24,14 @@ describe('URL compatibility', () => { cy.url().should('include', '/security/detections'); }); + + it('sets the global start and end dates from the url with timestamps', () => { + loginAndWaitForPageWithoutDateRange(ABSOLUTE_DATE_RANGE.urlWithTimestamps); + cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON).should( + 'have.attr', + 'title', + ABSOLUTE_DATE.startTime + ); + cy.get(DATE_PICKER_END_DATE_POPOVER_BUTTON).should('have.attr', 'title', ABSOLUTE_DATE.endTime); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts index 81af9ece9ed45..cdcdde252d6d6 100644 --- a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts @@ -42,24 +42,12 @@ import { HOSTS_URL } from '../urls/navigation'; import { ABSOLUTE_DATE_RANGE } from '../urls/state'; const ABSOLUTE_DATE = { - endTime: '1564691609186', - endTimeFormat: '2019-08-01T20:33:29.186Z', - endTimeTimeline: '1564779809186', - endTimeTimelineFormat: '2019-08-02T21:03:29.186Z', - endTimeTimelineTyped: 'Aug 02, 2019 @ 21:03:29.186', - endTimeTyped: 'Aug 01, 2019 @ 14:33:29.186', - newEndTime: '1564693409186', - newEndTimeFormat: '2019-08-01T21:03:29.186Z', + endTime: '2019-08-01T20:33:29.186Z', + endTimeTimeline: '2019-08-02T21:03:29.186Z', newEndTimeTyped: 'Aug 01, 2019 @ 15:03:29.186', - newStartTime: '1564691609186', - newStartTimeFormat: '2019-08-01T20:33:29.186Z', newStartTimeTyped: 'Aug 01, 2019 @ 14:33:29.186', - startTime: '1564689809186', - startTimeFormat: '2019-08-01T20:03:29.186Z', - startTimeTimeline: '1564776209186', - startTimeTimelineFormat: '2019-08-02T20:03:29.186Z', - startTimeTimelineTyped: 'Aug 02, 2019 @ 14:03:29.186', - startTimeTyped: 'Aug 01, 2019 @ 14:03:29.186', + startTime: '2019-08-01T20:03:29.186Z', + startTimeTimeline: '2019-08-02T20:03:29.186Z', }; describe('url state', () => { @@ -68,13 +56,9 @@ describe('url state', () => { cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON).should( 'have.attr', 'title', - ABSOLUTE_DATE.startTimeFormat - ); - cy.get(DATE_PICKER_END_DATE_POPOVER_BUTTON).should( - 'have.attr', - 'title', - ABSOLUTE_DATE.endTimeFormat + ABSOLUTE_DATE.startTime ); + cy.get(DATE_PICKER_END_DATE_POPOVER_BUTTON).should('have.attr', 'title', ABSOLUTE_DATE.endTime); }); it('sets the url state when start and end date are set', () => { @@ -87,9 +71,11 @@ describe('url state', () => { cy.url().should( 'include', - `(global:(linkTo:!(timeline),timerange:(from:${new Date( + `(global:(linkTo:!(timeline),timerange:(from:%27${new Date( ABSOLUTE_DATE.newStartTimeTyped - ).valueOf()},kind:absolute,to:${new Date(ABSOLUTE_DATE.newEndTimeTyped).valueOf()}))` + ).toISOString()}%27,kind:absolute,to:%27${new Date( + ABSOLUTE_DATE.newEndTimeTyped + ).toISOString()}%27))` ); }); @@ -100,12 +86,12 @@ describe('url state', () => { cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON_TIMELINE).should( 'have.attr', 'title', - ABSOLUTE_DATE.startTimeFormat + ABSOLUTE_DATE.startTime ); cy.get(DATE_PICKER_END_DATE_POPOVER_BUTTON_TIMELINE).should( 'have.attr', 'title', - ABSOLUTE_DATE.endTimeFormat + ABSOLUTE_DATE.endTime ); }); @@ -114,25 +100,21 @@ describe('url state', () => { cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON).should( 'have.attr', 'title', - ABSOLUTE_DATE.startTimeFormat - ); - cy.get(DATE_PICKER_END_DATE_POPOVER_BUTTON).should( - 'have.attr', - 'title', - ABSOLUTE_DATE.endTimeFormat + ABSOLUTE_DATE.startTime ); + cy.get(DATE_PICKER_END_DATE_POPOVER_BUTTON).should('have.attr', 'title', ABSOLUTE_DATE.endTime); openTimeline(); cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON_TIMELINE).should( 'have.attr', 'title', - ABSOLUTE_DATE.startTimeTimelineFormat + ABSOLUTE_DATE.startTimeTimeline ); cy.get(DATE_PICKER_END_DATE_POPOVER_BUTTON_TIMELINE).should( 'have.attr', 'title', - ABSOLUTE_DATE.endTimeTimelineFormat + ABSOLUTE_DATE.endTimeTimeline ); }); @@ -146,9 +128,11 @@ describe('url state', () => { cy.url().should( 'include', - `timeline:(linkTo:!(),timerange:(from:${new Date( + `timeline:(linkTo:!(),timerange:(from:%27${new Date( ABSOLUTE_DATE.newStartTimeTyped - ).valueOf()},kind:absolute,to:${new Date(ABSOLUTE_DATE.newEndTimeTyped).valueOf()}))` + ).toISOString()}%27,kind:absolute,to:%27${new Date( + ABSOLUTE_DATE.newEndTimeTyped + ).toISOString()}%27))` ); }); @@ -180,7 +164,7 @@ describe('url state', () => { cy.get(NETWORK).should( 'have.attr', 'href', - `/app/security/network?query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))` + `/app/security/network?query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))` ); }); @@ -193,12 +177,12 @@ describe('url state', () => { cy.get(HOSTS).should( 'have.attr', 'href', - `/app/security/hosts?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))` + `/app/security/hosts?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` ); cy.get(NETWORK).should( 'have.attr', 'href', - `/app/security/network?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))` + `/app/security/network?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` ); cy.get(HOSTS_NAMES).first().invoke('text').should('eq', 'siem-kibana'); @@ -209,21 +193,21 @@ describe('url state', () => { cy.get(ANOMALIES_TAB).should( 'have.attr', 'href', - "/app/security/hosts/siem-kibana/anomalies?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))" + "/app/security/hosts/siem-kibana/anomalies?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))" ); cy.get(BREADCRUMBS) .eq(1) .should( 'have.attr', 'href', - `/app/security/hosts?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))` + `/app/security/hosts?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` ); cy.get(BREADCRUMBS) .eq(2) .should( 'have.attr', 'href', - `/app/security/hosts/siem-kibana?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))` + `/app/security/hosts/siem-kibana?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` ); }); diff --git a/x-pack/plugins/security_solution/cypress/urls/state.ts b/x-pack/plugins/security_solution/cypress/urls/state.ts index bdd90c21fbedf..7825be08e38e1 100644 --- a/x-pack/plugins/security_solution/cypress/urls/state.ts +++ b/x-pack/plugins/security_solution/cypress/urls/state.ts @@ -6,16 +6,18 @@ export const ABSOLUTE_DATE_RANGE = { url: - '/app/security/network/flows/?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))', + '/app/security/network/flows/?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)))', + urlWithTimestamps: + '/app/security/network/flows/?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))', urlUnlinked: - '/app/security/network/flows/?timerange=(global:(linkTo:!(),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(),timerange:(from:1564776209186,kind:absolute,to:1564779809186)))', - urlKqlNetworkNetwork: `/app/security/network/flows/?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, - urlKqlNetworkHosts: `/app/security/network/flows/?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, - urlKqlHostsNetwork: `/app/security/hosts/allHosts?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, - urlKqlHostsHosts: `/app/security/hosts/allHosts?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, + '/app/security/network/flows/?timerange=(global:(linkTo:!(),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)),timeline:(linkTo:!(),timerange:(from:%272019-08-02T20:03:29.186Z%27,kind:absolute,to:%272019-08-02T21:03:29.186Z%27)))', + urlKqlNetworkNetwork: `/app/security/network/flows/?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)))`, + urlKqlNetworkHosts: `/app/security/network/flows/?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)))`, + urlKqlHostsNetwork: `/app/security/hosts/allHosts?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)))`, + urlKqlHostsHosts: `/app/security/hosts/allHosts?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)))`, urlHost: - '/app/security/hosts/authentications?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))', + '/app/security/hosts/authentications?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)))', urlHostNew: - '/app/security/hosts/authentications?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))', + '/app/security/hosts/authentications?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272020-01-01T21:33:29.186Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272020-01-01T21:33:29.186Z%27)))', }; diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx index bf2d8948b7292..841a1ef09ede6 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx @@ -17,9 +17,9 @@ import * as i18n from './translations'; import { useKibana } from '../../lib/kibana'; export interface OwnProps { - end: number; + end: string; id: string; - start: number; + start: string; } const defaultAlertsFilters: Filter[] = [ @@ -57,8 +57,8 @@ const defaultAlertsFilters: Filter[] = [ interface Props { timelineId: TimelineIdLiteral; - endDate: number; - startDate: number; + endDate: string; + startDate: string; pageFilters?: Filter[]; } diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index 38ca1176d1700..674eb3325efc2 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -15,29 +15,36 @@ import { mockEventViewerResponse } from './mock'; import { StatefulEventsViewer } from '.'; import { defaultHeaders } from './default_headers'; import { useFetchIndexPatterns } from '../../../detections/containers/detection_engine/rules/fetch_index_patterns'; -import { mockBrowserFields } from '../../containers/source/mock'; +import { mockBrowserFields, mockDocValueFields } from '../../containers/source/mock'; import { eventsDefaultModel } from './default_model'; import { useMountAppended } from '../../utils/use_mount_appended'; +jest.mock('../../components/url_state/normalize_time_range.ts'); + const mockUseFetchIndexPatterns: jest.Mock = useFetchIndexPatterns as jest.Mock; jest.mock('../../../detections/containers/detection_engine/rules/fetch_index_patterns'); -mockUseFetchIndexPatterns.mockImplementation(() => [ - { - browserFields: mockBrowserFields, - indexPatterns: mockIndexPattern, - }, -]); const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; jest.mock('use-resize-observer/polyfilled'); mockUseResizeObserver.mockImplementation(() => ({})); -const from = 1566943856794; -const to = 1566857456791; +const from = '2019-08-26T22:10:56.791Z'; +const to = '2019-08-27T22:10:56.794Z'; describe('EventsViewer', () => { const mount = useMountAppended(); + beforeEach(() => { + mockUseFetchIndexPatterns.mockImplementation(() => [ + { + browserFields: mockBrowserFields, + indexPatterns: mockIndexPattern, + docValueFields: mockDocValueFields, + isLoading: false, + }, + ]); + }); + test('it renders the "Showing..." subtitle with the expected event count', async () => { const wrapper = mount( @@ -60,6 +67,93 @@ describe('EventsViewer', () => { ); }); + test('it does NOT render fetch index pattern is loading', async () => { + mockUseFetchIndexPatterns.mockImplementation(() => [ + { + browserFields: mockBrowserFields, + indexPatterns: mockIndexPattern, + docValueFields: mockDocValueFields, + isLoading: true, + }, + ]); + + const wrapper = mount( + + + + + + ); + + await wait(); + wrapper.update(); + + expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe(false); + }); + + test('it does NOT render when start is empty', async () => { + mockUseFetchIndexPatterns.mockImplementation(() => [ + { + browserFields: mockBrowserFields, + indexPatterns: mockIndexPattern, + docValueFields: mockDocValueFields, + isLoading: true, + }, + ]); + + const wrapper = mount( + + + + + + ); + + await wait(); + wrapper.update(); + + expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe(false); + }); + + test('it does NOT render when end is empty', async () => { + mockUseFetchIndexPatterns.mockImplementation(() => [ + { + browserFields: mockBrowserFields, + indexPatterns: mockIndexPattern, + docValueFields: mockDocValueFields, + isLoading: true, + }, + ]); + + const wrapper = mount( + + + + + + ); + + await wait(); + wrapper.update(); + + expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe(false); + }); + test('it renders the Fields Browser as a settings gear', async () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index a81c5facb0718..5e0d5a6e9b099 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -10,7 +10,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; -import { BrowserFields } from '../../containers/source'; +import { BrowserFields, DocValueFields } from '../../containers/source'; import { TimelineQuery } from '../../../timelines/containers'; import { Direction } from '../../../graphql/types'; import { useKibana } from '../../lib/kibana'; @@ -51,19 +51,21 @@ interface Props { columns: ColumnHeaderOptions[]; dataProviders: DataProvider[]; deletedEventIds: Readonly; - end: number; + docValueFields: DocValueFields[]; + end: string; filters: Filter[]; headerFilterGroup?: React.ReactNode; height?: number; id: string; indexPattern: IIndexPattern; isLive: boolean; + isLoadingIndexPattern: boolean; itemsPerPage: number; itemsPerPageOptions: number[]; kqlMode: KqlMode; onChangeItemsPerPage: OnChangeItemsPerPage; query: Query; - start: number; + start: string; sort: Sort; toggleColumn: (column: ColumnHeaderOptions) => void; utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; @@ -76,6 +78,7 @@ const EventsViewerComponent: React.FC = ({ columns, dataProviders, deletedEventIds, + docValueFields, end, filters, headerFilterGroup, @@ -83,6 +86,7 @@ const EventsViewerComponent: React.FC = ({ id, indexPattern, isLive, + isLoadingIndexPattern, itemsPerPage, itemsPerPageOptions, kqlMode, @@ -122,6 +126,17 @@ const EventsViewerComponent: React.FC = ({ end, isEventViewer: true, }); + + const canQueryTimeline = useMemo( + () => + combinedQueries != null && + isLoadingIndexPattern != null && + !isLoadingIndexPattern && + !isEmpty(start) && + !isEmpty(end), + [isLoadingIndexPattern, combinedQueries, start, end] + ); + const fields = useMemo( () => union( @@ -140,16 +155,19 @@ const EventsViewerComponent: React.FC = ({ return ( - {combinedQueries != null ? ( + {canQueryTimeline ? ( {({ events, @@ -187,6 +205,7 @@ const EventsViewerComponent: React.FC = ({ !deletedEventIds.includes(e._id))} + docValueFields={docValueFields} id={id} isEventViewer={true} height={height} @@ -232,6 +251,7 @@ export const EventsViewer = React.memo( (prevProps, nextProps) => deepEqual(prevProps.browserFields, nextProps.browserFields) && prevProps.columns === nextProps.columns && + deepEqual(prevProps.docValueFields, nextProps.docValueFields) && prevProps.dataProviders === nextProps.dataProviders && prevProps.deletedEventIds === nextProps.deletedEventIds && prevProps.end === nextProps.end && diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx index a5f4dc0c5ed6f..1f820c0c748b6 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx @@ -18,6 +18,8 @@ import { useFetchIndexPatterns } from '../../../detections/containers/detection_ import { mockBrowserFields } from '../../containers/source/mock'; import { eventsDefaultModel } from './default_model'; +jest.mock('../../components/url_state/normalize_time_range.ts'); + const mockUseFetchIndexPatterns: jest.Mock = useFetchIndexPatterns as jest.Mock; jest.mock('../../../detections/containers/detection_engine/rules/fetch_index_patterns'); mockUseFetchIndexPatterns.mockImplementation(() => [ @@ -31,8 +33,8 @@ const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; jest.mock('use-resize-observer/polyfilled'); mockUseResizeObserver.mockImplementation(() => ({})); -const from = 1566943856794; -const to = 1566857456791; +const from = '2019-08-27T22:10:56.794Z'; +const to = '2019-08-26T22:10:56.791Z'; describe('StatefulEventsViewer', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 637f1a48143a9..6c610a084e7f2 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -27,9 +27,9 @@ import { InspectButtonContainer } from '../inspect'; export interface OwnProps { defaultIndices?: string[]; defaultModel: SubsetTimelineModel; - end: number; + end: string; id: string; - start: number; + start: string; headerFilterGroup?: React.ReactNode; pageFilters?: Filter[]; utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; @@ -65,9 +65,9 @@ const StatefulEventsViewerComponent: React.FC = ({ // If truthy, the graph viewer (Resolver) is showing graphEventId, }) => { - const [{ browserFields, indexPatterns }] = useFetchIndexPatterns( - defaultIndices ?? useUiSetting(DEFAULT_INDEX_KEY) - ); + const [ + { docValueFields, browserFields, indexPatterns, isLoading: isLoadingIndexPattern }, + ] = useFetchIndexPatterns(defaultIndices ?? useUiSetting(DEFAULT_INDEX_KEY)); useEffect(() => { if (createTimeline != null) { @@ -120,10 +120,12 @@ const StatefulEventsViewerComponent: React.FC = ({ { const mockMatrixOverTimeHistogramProps = { defaultIndex: ['defaultIndex'], defaultStackByOption: { text: 'text', value: 'value' }, - endDate: new Date('2019-07-18T20:00:00.000Z').valueOf(), + endDate: '2019-07-18T20:00:00.000Z', errorMessage: 'error', histogramType: HistogramType.alerts, id: 'mockId', @@ -64,7 +64,7 @@ describe('Matrix Histogram Component', () => { sourceId: 'default', stackByField: 'mockStackByField', stackByOptions: [{ text: 'text', value: 'value' }], - startDate: new Date('2019-07-18T19:00: 00.000Z').valueOf(), + startDate: '2019-07-18T19:00: 00.000Z', subtitle: 'mockSubtitle', totalCount: -1, title: 'mockTitle', diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx index 16fe2a6669ff0..fa512ad1ed80b 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx @@ -115,8 +115,8 @@ export const MatrixHistogramComponent: React.FC< const [min, max] = x; dispatchSetAbsoluteRangeDatePicker({ id: setAbsoluteRangeDatePickerTarget, - from: min, - to: max, + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), }); }, yTickFormatter, diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts index ff0816758cb0c..a859b0dd39231 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts @@ -44,8 +44,8 @@ interface MatrixHistogramBasicProps { defaultStackByOption: MatrixHistogramOption; dispatchSetAbsoluteRangeDatePicker: ActionCreator<{ id: InputsModelId; - from: number; - to: number; + from: string; + to: string; }>; endDate: GlobalTimeArgs['to']; headerChildren?: React.ReactNode; @@ -63,17 +63,17 @@ interface MatrixHistogramBasicProps { } export interface MatrixHistogramQueryProps { - endDate: number; + endDate: string; errorMessage: string; filterQuery?: ESQuery | string | undefined; setAbsoluteRangeDatePicker?: ActionCreator<{ id: InputsModelId; - from: number; - to: number; + from: string; + to: string; }>; setAbsoluteRangeDatePickerTarget?: InputsModelId; stackByField: string; - startDate: number; + startDate: string; indexToAdd?: string[] | null; isInspected: boolean; histogramType: HistogramType; diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.test.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.test.ts index 9e3ddcc014c61..7a3f44d3ea729 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.test.ts @@ -22,8 +22,8 @@ describe('utils', () => { let configs: BarchartConfigs; beforeAll(() => { configs = getBarchartConfigs({ - from: 0, - to: 0, + from: '2020-07-07T08:20:18.966Z', + to: '2020-07-08T08:20:18.966Z', onBrushEnd: jest.fn() as UpdateDateRange, }); }); @@ -53,8 +53,8 @@ describe('utils', () => { beforeAll(() => { configs = getBarchartConfigs({ chartHeight: mockChartHeight, - from: 0, - to: 0, + from: '2020-07-07T08:20:18.966Z', + to: '2020-07-08T08:20:18.966Z', onBrushEnd: jest.fn() as UpdateDateRange, yTickFormatter: mockYTickFormatter, showLegend: false, diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.ts index 45e9c54b2eff8..9474929d35a51 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.ts @@ -13,9 +13,9 @@ import { histogramDateTimeFormatter } from '../utils'; interface GetBarchartConfigsProps { chartHeight?: number; - from: number; + from: string; legendPosition?: Position; - to: number; + to: string; onBrushEnd: UpdateDateRange; yTickFormatter?: (value: number) => string; showLegend?: boolean; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/anomaly_table_provider.tsx b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/anomaly_table_provider.tsx index 6ccc41546e558..66e70ddc2e14f 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/anomaly_table_provider.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/anomaly_table_provider.tsx @@ -15,8 +15,8 @@ interface ChildrenArgs { interface Props { influencers?: InfluencerInput[]; - startDate: number; - endDate: number; + startDate: string; + endDate: string; criteriaFields?: CriteriaFields[]; children: (args: ChildrenArgs) => React.ReactNode; skip: boolean; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts index 8568c7e6b5575..a6bbdee79cf04 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { DEFAULT_ANOMALY_SCORE } from '../../../../../common/constants'; import { anomaliesTableData } from '../api/anomalies_table_data'; @@ -19,8 +19,8 @@ import { useTimeZone, useUiSetting$ } from '../../../lib/kibana'; interface Args { influencers?: InfluencerInput[]; - endDate: number; - startDate: number; + endDate: string; + startDate: string; threshold?: number; skip?: boolean; criteriaFields?: CriteriaFields[]; @@ -67,6 +67,8 @@ export const useAnomaliesTableData = ({ const [anomalyScore] = useUiSetting$(DEFAULT_ANOMALY_SCORE); const siemJobIds = siemJobs.filter((job) => job.isInstalled).map((job) => job.id); + const startDateMs = useMemo(() => new Date(startDate).getTime(), [startDate]); + const endDateMs = useMemo(() => new Date(endDate).getTime(), [endDate]); useEffect(() => { let isSubscribed = true; @@ -116,7 +118,7 @@ export const useAnomaliesTableData = ({ } } - fetchAnomaliesTableData(influencers, criteriaFields, startDate, endDate); + fetchAnomaliesTableData(influencers, criteriaFields, startDateMs, endDateMs); return () => { isSubscribed = false; abortCtrl.abort(); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.test.ts index 4a25f82a94a61..30d0673192af8 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.test.ts @@ -18,8 +18,8 @@ describe('create_explorer_link', () => { test('it returns expected link', () => { const entities = createExplorerLink( anomalies.anomalies[0], - new Date('1970').valueOf(), - new Date('3000').valueOf() + new Date('1970').toISOString(), + new Date('3000').toISOString() ); expect(entities).toEqual( "#/explorer?_g=(ml:(jobIds:!(job-1)),refreshInterval:(display:Off,pause:!f,value:0),time:(from:'1970-01-01T00:00:00.000Z',mode:absolute,to:'3000-01-01T00:00:00.000Z'))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:(),mlSelectLimit:(display:'10',val:10),mlShowCharts:!t)" diff --git a/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.tsx b/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.tsx index e00f53a08a918..468bc962453f6 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.tsx @@ -11,8 +11,8 @@ import { useKibana } from '../../../lib/kibana'; interface ExplorerLinkProps { score: Anomaly; - startDate: number; - endDate: number; + startDate: string; + endDate: string; linkName: React.ReactNode; } @@ -35,7 +35,7 @@ export const ExplorerLink: React.FC = ({ ); }; -export const createExplorerLink = (score: Anomaly, startDate: number, endDate: number): string => { +export const createExplorerLink = (score: Anomaly, startDate: string, endDate: string): string => { const startDateIso = new Date(startDate).toISOString(); const endDateIso = new Date(endDate).toISOString(); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap index 6694cec53987b..0abb94f6e92ff 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap @@ -127,7 +127,7 @@ exports[`anomaly_scores renders correctly against snapshot 1`] = ` grow={false} > , diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_scores.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_scores.test.tsx.snap index de9ae94c4d95e..b9e4a76363a40 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_scores.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_scores.test.tsx.snap @@ -7,7 +7,7 @@ exports[`anomaly_scores renders correctly against snapshot 1`] = ` responsive={false} > diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/create_descriptions_list.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/create_descriptions_list.test.tsx.snap index 2e771f9f045b8..5d052ef028e0f 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/create_descriptions_list.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/create_descriptions_list.test.tsx.snap @@ -44,7 +44,7 @@ exports[`create_description_list renders correctly against snapshot 1`] = ` grow={false} > diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx index b172c22a9ed4e..f7fa0ac0a8be1 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx @@ -13,7 +13,9 @@ import { TestProviders } from '../../../mock/test_providers'; import { useMountAppended } from '../../../utils/use_mount_appended'; import { Anomalies } from '../types'; -const endDate: number = new Date('3000-01-01T00:00:00.000Z').valueOf(); +const startDate: string = '2020-07-07T08:20:18.966Z'; +const endDate: string = '3000-01-01T00:00:00.000Z'; + const narrowDateRange = jest.fn(); describe('anomaly_scores', () => { @@ -28,7 +30,7 @@ describe('anomaly_scores', () => { const wrapper = shallow( { { { @@ -29,7 +30,7 @@ describe('anomaly_scores', () => { const wrapper = shallow( { { { { { { { diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/create_descriptions_list.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/create_descriptions_list.test.tsx index 7c8900bf77d95..e9dd5f922e26a 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/create_descriptions_list.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/create_descriptions_list.test.tsx @@ -13,7 +13,8 @@ import { Anomaly } from '../types'; jest.mock('../../../lib/kibana'); -const endDate: number = new Date('3000-01-01T00:00:00.000Z').valueOf(); +const startDate: string = '2020-07-07T08:20:18.966Z'; +const endDate: string = '3000-01-01T00:00:00.000Z'; describe('create_description_list', () => { let narrowDateRange = jest.fn(); @@ -27,7 +28,7 @@ describe('create_description_list', () => { { { { { test('converts a second interval to plus or minus (+/-) one hour', () => { const expected: FromTo = { - from: new Date('2019-06-25T04:31:59.345Z').valueOf(), - to: new Date('2019-06-25T06:31:59.345Z').valueOf(), + from: '2019-06-25T04:31:59.345Z', + to: '2019-06-25T06:31:59.345Z', }; anomalies.anomalies[0].time = new Date('2019-06-25T05:31:59.345Z').valueOf(); expect(scoreIntervalToDateTime(anomalies.anomalies[0], 'second')).toEqual(expected); @@ -26,8 +26,8 @@ describe('score_interval_to_datetime', () => { test('converts a minute interval to plus or minus (+/-) one hour', () => { const expected: FromTo = { - from: new Date('2019-06-25T04:31:59.345Z').valueOf(), - to: new Date('2019-06-25T06:31:59.345Z').valueOf(), + from: '2019-06-25T04:31:59.345Z', + to: '2019-06-25T06:31:59.345Z', }; anomalies.anomalies[0].time = new Date('2019-06-25T05:31:59.345Z').valueOf(); expect(scoreIntervalToDateTime(anomalies.anomalies[0], 'minute')).toEqual(expected); @@ -35,8 +35,8 @@ describe('score_interval_to_datetime', () => { test('converts a hour interval to plus or minus (+/-) one hour', () => { const expected: FromTo = { - from: new Date('2019-06-25T04:31:59.345Z').valueOf(), - to: new Date('2019-06-25T06:31:59.345Z').valueOf(), + from: '2019-06-25T04:31:59.345Z', + to: '2019-06-25T06:31:59.345Z', }; anomalies.anomalies[0].time = new Date('2019-06-25T05:31:59.345Z').valueOf(); expect(scoreIntervalToDateTime(anomalies.anomalies[0], 'hour')).toEqual(expected); @@ -44,8 +44,8 @@ describe('score_interval_to_datetime', () => { test('converts a day interval to plus or minus (+/-) one day', () => { const expected: FromTo = { - from: new Date('2019-06-24T05:31:59.345Z').valueOf(), - to: new Date('2019-06-26T05:31:59.345Z').valueOf(), + from: '2019-06-24T05:31:59.345Z', + to: '2019-06-26T05:31:59.345Z', }; anomalies.anomalies[0].time = new Date('2019-06-25T05:31:59.345Z').valueOf(); expect(scoreIntervalToDateTime(anomalies.anomalies[0], 'day')).toEqual(expected); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/score_interval_to_datetime.ts b/x-pack/plugins/security_solution/public/common/components/ml/score/score_interval_to_datetime.ts index b1257676a64b2..69b5be9272a38 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/score_interval_to_datetime.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/score_interval_to_datetime.ts @@ -8,21 +8,21 @@ import moment from 'moment'; import { Anomaly } from '../types'; export interface FromTo { - from: number; - to: number; + from: string; + to: string; } export const scoreIntervalToDateTime = (score: Anomaly, interval: string): FromTo => { if (interval === 'second' || interval === 'minute' || interval === 'hour') { return { - from: moment(score.time).subtract(1, 'hour').valueOf(), - to: moment(score.time).add(1, 'hour').valueOf(), + from: moment(score.time).subtract(1, 'hour').toISOString(), + to: moment(score.time).add(1, 'hour').toISOString(), }; } else { // default should be a day return { - from: moment(score.time).subtract(1, 'day').valueOf(), - to: moment(score.time).add(1, 'day').valueOf(), + from: moment(score.time).subtract(1, 'day').toISOString(), + to: moment(score.time).add(1, 'day').toISOString(), }; } }; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx index 93b22460d4ed7..b90946c534f3a 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx @@ -13,8 +13,8 @@ import { TestProviders } from '../../../mock'; import React from 'react'; import { useMountAppended } from '../../../utils/use_mount_appended'; -const startDate = new Date(2001).valueOf(); -const endDate = new Date(3000).valueOf(); +const startDate = new Date(2001).toISOString(); +const endDate = new Date(3000).toISOString(); const interval = 'days'; const narrowDateRange = jest.fn(); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx index fc89189bf4f46..b72da55128f99 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx @@ -24,8 +24,8 @@ import { escapeDataProviderId } from '../../drag_and_drop/helpers'; import { FormattedRelativePreferenceDate } from '../../formatted_date'; export const getAnomaliesHostTableColumns = ( - startDate: number, - endDate: number, + startDate: string, + endDate: string, interval: string, narrowDateRange: NarrowDateRange ): [ @@ -132,8 +132,8 @@ export const getAnomaliesHostTableColumns = ( export const getAnomaliesHostTableColumnsCurated = ( pageType: HostsType, - startDate: number, - endDate: number, + startDate: string, + endDate: string, interval: string, narrowDateRange: NarrowDateRange ) => { diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx index b113c692c535a..79277c46e1c9d 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx @@ -13,8 +13,8 @@ import React from 'react'; import { TestProviders } from '../../../mock'; import { useMountAppended } from '../../../utils/use_mount_appended'; -const startDate = new Date(2001).valueOf(); -const endDate = new Date(3000).valueOf(); +const startDate = new Date(2001).toISOString(); +const endDate = new Date(3000).toISOString(); describe('get_anomalies_network_table_columns', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx index ce4269afbe5b2..52b26a20a8f64 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx @@ -26,8 +26,8 @@ import { escapeDataProviderId } from '../../drag_and_drop/helpers'; import { FlowTarget } from '../../../../graphql/types'; export const getAnomaliesNetworkTableColumns = ( - startDate: number, - endDate: number, + startDate: string, + endDate: string, flowTarget?: FlowTarget ): [ Columns, @@ -127,8 +127,8 @@ export const getAnomaliesNetworkTableColumns = ( export const getAnomaliesNetworkTableColumnsCurated = ( pageType: NetworkType, - startDate: number, - endDate: number, + startDate: string, + endDate: string, flowTarget?: FlowTarget ) => { const columns = getAnomaliesNetworkTableColumns(startDate, endDate, flowTarget); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/host_equality.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/tables/host_equality.test.ts index 89b87f95e5159..eaaf5a9aedcdb 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/host_equality.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/host_equality.test.ts @@ -11,15 +11,15 @@ import { HostsType } from '../../../../hosts/store/model'; describe('host_equality', () => { test('it returns true if start and end date are equal', () => { const prev: AnomaliesHostTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: HostsType.details, }; const next: AnomaliesHostTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: HostsType.details, @@ -30,15 +30,15 @@ describe('host_equality', () => { test('it returns false if starts are not equal', () => { const prev: AnomaliesHostTableProps = { - startDate: new Date('2001').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2001').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: HostsType.details, }; const next: AnomaliesHostTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: HostsType.details, @@ -49,15 +49,15 @@ describe('host_equality', () => { test('it returns false if starts are not equal for next', () => { const prev: AnomaliesHostTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: HostsType.details, }; const next: AnomaliesHostTableProps = { - startDate: new Date('2001').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2001').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: HostsType.details, @@ -68,15 +68,15 @@ describe('host_equality', () => { test('it returns false if ends are not equal', () => { const prev: AnomaliesHostTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2001').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2001').toISOString(), narrowDateRange: jest.fn(), skip: false, type: HostsType.details, }; const next: AnomaliesHostTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: HostsType.details, @@ -87,15 +87,15 @@ describe('host_equality', () => { test('it returns false if ends are not equal for next', () => { const prev: AnomaliesHostTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: HostsType.details, }; const next: AnomaliesHostTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2001').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2001').toISOString(), narrowDateRange: jest.fn(), skip: false, type: HostsType.details, @@ -106,15 +106,15 @@ describe('host_equality', () => { test('it returns false if skip is not equal', () => { const prev: AnomaliesHostTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: true, type: HostsType.details, }; const next: AnomaliesHostTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: HostsType.details, diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/network_equality.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/tables/network_equality.test.ts index 8b3e30c329031..3819e9d0e4b3f 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/network_equality.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/network_equality.test.ts @@ -12,15 +12,15 @@ import { FlowTarget } from '../../../../graphql/types'; describe('network_equality', () => { test('it returns true if start and end date are equal', () => { const prev: AnomaliesNetworkTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: NetworkType.details, }; const next: AnomaliesNetworkTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: NetworkType.details, @@ -31,15 +31,15 @@ describe('network_equality', () => { test('it returns false if starts are not equal', () => { const prev: AnomaliesNetworkTableProps = { - startDate: new Date('2001').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2001').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: NetworkType.details, }; const next: AnomaliesNetworkTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: NetworkType.details, @@ -50,15 +50,15 @@ describe('network_equality', () => { test('it returns false if starts are not equal for next', () => { const prev: AnomaliesNetworkTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: NetworkType.details, }; const next: AnomaliesNetworkTableProps = { - startDate: new Date('2001').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2001').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: NetworkType.details, @@ -69,15 +69,15 @@ describe('network_equality', () => { test('it returns false if ends are not equal', () => { const prev: AnomaliesNetworkTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2001').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2001').toISOString(), narrowDateRange: jest.fn(), skip: false, type: NetworkType.details, }; const next: AnomaliesNetworkTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: NetworkType.details, @@ -88,15 +88,15 @@ describe('network_equality', () => { test('it returns false if ends are not equal for next', () => { const prev: AnomaliesNetworkTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: NetworkType.details, }; const next: AnomaliesNetworkTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2001').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2001').toISOString(), narrowDateRange: jest.fn(), skip: false, type: NetworkType.details, @@ -107,15 +107,15 @@ describe('network_equality', () => { test('it returns false if skip is not equal', () => { const prev: AnomaliesNetworkTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: true, type: NetworkType.details, }; const next: AnomaliesNetworkTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: NetworkType.details, @@ -126,16 +126,16 @@ describe('network_equality', () => { test('it returns false if flowType is not equal', () => { const prev: AnomaliesNetworkTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: true, type: NetworkType.details, flowTarget: FlowTarget.source, }; const next: AnomaliesNetworkTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: NetworkType.details, diff --git a/x-pack/plugins/security_solution/public/common/components/ml/types.ts b/x-pack/plugins/security_solution/public/common/components/ml/types.ts index 13bceaa473a84..a4c4f728b0f8f 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/types.ts @@ -75,8 +75,8 @@ export interface AnomaliesByNetwork { } export interface HostOrNetworkProps { - startDate: number; - endDate: number; + startDate: string; + endDate: string; narrowDateRange: NarrowDateRange; skip: boolean; } diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts index ade76f8e24338..7e508c28c62df 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts @@ -80,20 +80,20 @@ const getMockObject = ( global: { linkTo: ['timeline'], timerange: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, }, timeline: { linkTo: ['global'], timerange: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, }, @@ -123,7 +123,7 @@ describe('Navigation Breadcrumbs', () => { }, { href: - 'securitySolution:hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + "securitySolution:hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", text: 'Hosts', }, { @@ -143,7 +143,7 @@ describe('Navigation Breadcrumbs', () => { { text: 'Network', href: - 'securitySolution:network?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + "securitySolution:network?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, { text: 'Flows', @@ -162,7 +162,7 @@ describe('Navigation Breadcrumbs', () => { { text: 'Timelines', href: - 'securitySolution:timelines?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + "securitySolution:timelines?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, ]); }); @@ -177,12 +177,12 @@ describe('Navigation Breadcrumbs', () => { { text: 'Hosts', href: - 'securitySolution:hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + "securitySolution:hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, { text: 'siem-kibana', href: - 'securitySolution:hosts/siem-kibana?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + "securitySolution:hosts/siem-kibana?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, { text: 'Authentications', href: '' }, ]); @@ -198,11 +198,11 @@ describe('Navigation Breadcrumbs', () => { { text: 'Network', href: - 'securitySolution:network?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + "securitySolution:network?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, { text: ipv4, - href: `securitySolution:network/ip/${ipv4}/source?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))`, + href: `securitySolution:network/ip/${ipv4}/source?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, }, { text: 'Flows', href: '' }, ]); @@ -218,11 +218,11 @@ describe('Navigation Breadcrumbs', () => { { text: 'Network', href: - 'securitySolution:network?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + "securitySolution:network?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, { text: ipv6, - href: `securitySolution:network/ip/${ipv6Encoded}/source?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))`, + href: `securitySolution:network/ip/${ipv6Encoded}/source?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, }, { text: 'Flows', href: '' }, ]); @@ -237,12 +237,12 @@ describe('Navigation Breadcrumbs', () => { { text: 'Hosts', href: - 'securitySolution:hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + "securitySolution:hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, { text: 'siem-kibana', href: - 'securitySolution:hosts/siem-kibana?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + "securitySolution:hosts/siem-kibana?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, { text: 'Authentications', href: '' }, ]); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx index c60feb63241fb..16cb19f5a0c14 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx @@ -57,20 +57,20 @@ describe('SIEM Navigation', () => { [CONSTANTS.timerange]: { global: { [CONSTANTS.timerange]: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, linkTo: ['timeline'], }, timeline: { [CONSTANTS.timerange]: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, linkTo: ['global'], @@ -160,20 +160,20 @@ describe('SIEM Navigation', () => { global: { linkTo: ['timeline'], timerange: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, }, timeline: { linkTo: ['global'], timerange: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, }, @@ -259,20 +259,20 @@ describe('SIEM Navigation', () => { global: { linkTo: ['timeline'], timerange: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, }, timeline: { linkTo: ['global'], timerange: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, }, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx index f345346d620cb..b25cf3779801b 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx @@ -47,20 +47,20 @@ describe('Tab Navigation', () => { [CONSTANTS.timerange]: { global: { [CONSTANTS.timerange]: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, linkTo: ['timeline'], }, timeline: { [CONSTANTS.timerange]: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, linkTo: ['global'], @@ -105,20 +105,20 @@ describe('Tab Navigation', () => { [CONSTANTS.timerange]: { global: { [CONSTANTS.timerange]: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, linkTo: ['timeline'], }, timeline: { [CONSTANTS.timerange]: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, linkTo: ['global'], diff --git a/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx index f548275b36e70..8a78706e17a4c 100644 --- a/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx @@ -41,8 +41,8 @@ import { State, createStore } from '../../store'; import { Provider as ReduxStoreProvider } from 'react-redux'; import { KpiNetworkData, KpiHostsData } from '../../../graphql/types'; -const from = new Date('2019-06-15T06:00:00.000Z').valueOf(); -const to = new Date('2019-06-18T06:00:00.000Z').valueOf(); +const from = '2019-06-15T06:00:00.000Z'; +const to = '2019-06-18T06:00:00.000Z'; jest.mock('../charts/areachart', () => { return { AreaChart: () =>
}; @@ -131,18 +131,18 @@ describe('Stat Items Component', () => { { key: 'uniqueSourceIpsHistogram', value: [ - { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 565975 }, - { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1084366 }, - { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12280 }, + { x: new Date('2019-05-03T13:00:00.000Z').toISOString(), y: 565975 }, + { x: new Date('2019-05-04T01:00:00.000Z').toISOString(), y: 1084366 }, + { x: new Date('2019-05-04T13:00:00.000Z').toISOString(), y: 12280 }, ], color: '#D36086', }, { key: 'uniqueDestinationIpsHistogram', value: [ - { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 565975 }, - { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1084366 }, - { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12280 }, + { x: new Date('2019-05-03T13:00:00.000Z').toISOString(), y: 565975 }, + { x: new Date('2019-05-04T01:00:00.000Z').toISOString(), y: 1084366 }, + { x: new Date('2019-05-04T13:00:00.000Z').toISOString(), y: 12280 }, ], color: '#9170B8', }, diff --git a/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx b/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx index dee730059b03a..183f89d9320f3 100644 --- a/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx @@ -66,10 +66,10 @@ export interface StatItems { export interface StatItemsProps extends StatItems { areaChart?: ChartSeriesData[]; barChart?: ChartSeriesData[]; - from: number; + from: string; id: string; narrowDateRange: UpdateDateRange; - to: number; + to: string; } export const numberFormatter = (value: string | number): string => value.toLocaleString(); @@ -160,8 +160,8 @@ export const useKpiMatrixStatus = ( mappings: Readonly, data: KpiHostsData | KpiNetworkData, id: string, - from: number, - to: number, + from: string, + to: string, narrowDateRange: UpdateDateRange ): StatItemsProps[] => { const [statItemsProps, setStatItemsProps] = useState(mappings as StatItemsProps[]); diff --git a/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx index 164ca177ee91a..0795e46c9e45f 100644 --- a/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx @@ -156,8 +156,8 @@ describe('SIEM Super Date Picker', () => { }); test('Make Sure to (end date) is superior than from (start date)', () => { - expect(store.getState().inputs.global.timerange.to).toBeGreaterThan( - store.getState().inputs.global.timerange.from + expect(new Date(store.getState().inputs.global.timerange.to).valueOf()).toBeGreaterThan( + new Date(store.getState().inputs.global.timerange.from).valueOf() ); }); }); @@ -321,7 +321,7 @@ describe('SIEM Super Date Picker', () => { const mapStateToProps = makeMapStateToProps(); const props1 = mapStateToProps(state, { id: 'global' }); const clone = cloneDeep(state); - clone.inputs.global.timerange.from = 999; + clone.inputs.global.timerange.from = '2020-07-07T09:20:18.966Z'; const props2 = mapStateToProps(clone, { id: 'global' }); expect(props1.start).not.toBe(props2.start); }); @@ -330,7 +330,7 @@ describe('SIEM Super Date Picker', () => { const mapStateToProps = makeMapStateToProps(); const props1 = mapStateToProps(state, { id: 'global' }); const clone = cloneDeep(state); - clone.inputs.global.timerange.to = 999; + clone.inputs.global.timerange.to = '2020-07-08T09:20:18.966Z'; const props2 = mapStateToProps(clone, { id: 'global' }); expect(props1.end).not.toBe(props2.end); }); diff --git a/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.tsx b/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.tsx index 84ff1120f6496..4443d24531b22 100644 --- a/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.tsx @@ -216,9 +216,9 @@ export const formatDate = ( options?: { roundUp?: boolean; } -) => { +): string => { const momentDate = dateMath.parse(date, options); - return momentDate != null && momentDate.isValid() ? momentDate.valueOf() : 0; + return momentDate != null && momentDate.isValid() ? momentDate.toISOString() : ''; }; export const dispatchUpdateReduxTime = (dispatch: Dispatch) => ({ diff --git a/x-pack/plugins/security_solution/public/common/components/super_date_picker/selectors.test.ts b/x-pack/plugins/security_solution/public/common/components/super_date_picker/selectors.test.ts index 1dafa141542bf..7cb4ea9ada93f 100644 --- a/x-pack/plugins/security_solution/public/common/components/super_date_picker/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/super_date_picker/selectors.test.ts @@ -23,8 +23,8 @@ describe('selectors', () => { kind: 'absolute', fromStr: undefined, toStr: undefined, - from: 0, - to: 0, + from: '2020-07-07T08:20:18.966Z', + to: '2020-07-08T08:20:18.966Z', }; let inputState: InputsRange = { @@ -57,8 +57,8 @@ describe('selectors', () => { kind: 'absolute', fromStr: undefined, toStr: undefined, - from: 0, - to: 0, + from: '2020-07-07T08:20:18.966Z', + to: '2020-07-08T08:20:18.966Z', }; inputState = { @@ -147,8 +147,8 @@ describe('selectors', () => { kind: 'relative', fromStr: '', toStr: '', - from: 1, - to: 0, + from: '2020-07-08T08:20:18.966Z', + to: '2020-07-09T08:20:18.966Z', }; const change: InputsRange = { ...inputState, @@ -179,8 +179,8 @@ describe('selectors', () => { kind: 'relative', fromStr: '', toStr: '', - from: 1, - to: 0, + from: '2020-07-08T08:20:18.966Z', + to: '2020-07-09T08:20:18.966Z', }; const change: InputsRange = { ...inputState, @@ -211,8 +211,8 @@ describe('selectors', () => { kind: 'relative', fromStr: '', toStr: '', - from: 0, - to: 1, + from: '2020-07-08T08:20:18.966Z', + to: '2020-07-09T08:20:18.966Z', }; const change: InputsRange = { ...inputState, @@ -243,8 +243,8 @@ describe('selectors', () => { kind: 'relative', fromStr: '', toStr: '', - from: 0, - to: 0, + from: '2020-07-08T08:20:18.966Z', + to: '2020-07-09T08:20:18.966Z', }; const change: InputsRange = { ...inputState, @@ -275,8 +275,8 @@ describe('selectors', () => { kind: 'relative', fromStr: '', toStr: '', - from: 0, - to: 0, + from: '2020-07-08T08:20:18.966Z', + to: '2020-07-09T08:20:18.966Z', }; const change: InputsRange = { ...inputState, diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx index c8232b0c3b3cb..b393e9ae6319b 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx @@ -88,8 +88,8 @@ const state: State = { kind: 'relative', fromStr: 'now-24h', toStr: 'now', - from: 1586835969047, - to: 1586922369047, + from: '2020-04-14T03:46:09.047Z', + to: '2020-04-15T03:46:09.047Z', }, }, }, @@ -242,7 +242,7 @@ describe('StatefulTopN', () => { test(`provides 'from' via GlobalTime when rendering in a global context`, () => { const props = wrapper.find('[data-test-subj="top-n"]').first().props() as Props; - expect(props.from).toEqual(0); + expect(props.from).toEqual('2020-07-07T08:20:18.966Z'); }); test('provides the global query from Redux state (inputs > global > query) when rendering in a global context', () => { @@ -260,7 +260,7 @@ describe('StatefulTopN', () => { test(`provides 'to' via GlobalTime when rendering in a global context`, () => { const props = wrapper.find('[data-test-subj="top-n"]').first().props() as Props; - expect(props.to).toEqual(1); + expect(props.to).toEqual('2020-07-08T08:20:18.966Z'); }); }); @@ -298,7 +298,7 @@ describe('StatefulTopN', () => { const props = wrapper.find('[data-test-subj="top-n"]').first().props() as Props; expect(props.combinedQueries).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"network.transport":"tcp"}}],"minimum_should_match":1}},{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1586835969047}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1586922369047}}}],"minimum_should_match":1}}]}}]}},{"match_phrase":{"source.port":{"query":"30045"}}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"network.transport":"tcp"}}],"minimum_should_match":1}},{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}]}},{"match_phrase":{"source.port":{"query":"30045"}}}],"should":[],"must_not":[]}}' ); }); @@ -323,7 +323,7 @@ describe('StatefulTopN', () => { test(`provides 'from' via redux state (inputs > timeline > timerange) when rendering in a timeline context`, () => { const props = wrapper.find('[data-test-subj="top-n"]').first().props() as Props; - expect(props.from).toEqual(1586835969047); + expect(props.from).toEqual('2020-04-14T03:46:09.047Z'); }); test('provides an empty query when rendering in a timeline context', () => { @@ -341,7 +341,7 @@ describe('StatefulTopN', () => { test(`provides 'to' via redux state (inputs > timeline > timerange) when rendering in a timeline context`, () => { const props = wrapper.find('[data-test-subj="top-n"]').first().props() as Props; - expect(props.to).toEqual(1586922369047); + expect(props.to).toEqual('2020-04-15T03:46:09.047Z'); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx index b1979c501c778..e5a1fb6120285 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx @@ -114,14 +114,14 @@ describe('TopN', () => { defaultView="raw" field={field} filters={[]} - from={1586824307695} + from={'2020-04-14T00:31:47.695Z'} indexPattern={mockIndexPattern} options={defaultOptions} query={query} setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} setAbsoluteRangeDatePickerTarget="global" setQuery={jest.fn()} - to={1586910707695} + to={'2020-04-15T00:31:47.695Z'} toggleTopN={toggleTopN} value={value} /> @@ -153,14 +153,14 @@ describe('TopN', () => { defaultView="raw" field={field} filters={[]} - from={1586824307695} + from={'2020-04-14T00:31:47.695Z'} indexPattern={mockIndexPattern} options={defaultOptions} query={query} setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} setAbsoluteRangeDatePickerTarget="global" setQuery={jest.fn()} - to={1586910707695} + to={'2020-04-15T00:31:47.695Z'} toggleTopN={toggleTopN} value={value} /> @@ -191,14 +191,14 @@ describe('TopN', () => { defaultView="alert" field={field} filters={[]} - from={1586824307695} + from={'2020-04-14T00:31:47.695Z'} indexPattern={mockIndexPattern} options={defaultOptions} query={query} setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} setAbsoluteRangeDatePickerTarget="global" setQuery={jest.fn()} - to={1586910707695} + to={'2020-04-15T00:31:47.695Z'} toggleTopN={toggleTopN} value={value} /> @@ -228,14 +228,14 @@ describe('TopN', () => { defaultView="all" field={field} filters={[]} - from={1586824307695} + from={'2020-04-14T00:31:47.695Z'} indexPattern={mockIndexPattern} options={allEvents} query={query} setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} setAbsoluteRangeDatePickerTarget="global" setQuery={jest.fn()} - to={1586910707695} + to={'2020-04-15T00:31:47.695Z'} toggleTopN={jest.fn()} value={value} /> diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx index 5e2fd998224c6..064241a7216f4 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx @@ -54,8 +54,8 @@ export interface Props extends Pick; setAbsoluteRangeDatePickerTarget: InputsModelId; timelineId?: string; diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/__mocks__/normalize_time_range.ts b/x-pack/plugins/security_solution/public/common/components/url_state/__mocks__/normalize_time_range.ts new file mode 100644 index 0000000000000..37c839c2969d4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/url_state/__mocks__/normalize_time_range.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const normalizeTimeRange = () => ({ + from: '2020-07-07T08:20:18.966Z', + to: '2020-07-08T08:20:18.966Z', +}); diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx index eeeaacc25a15e..9d0d9e7b250a0 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx @@ -38,7 +38,7 @@ jest.mock('../../utils/route/use_route_spy', () => ({ jest.mock('../super_date_picker', () => ({ formatDate: (date: string) => { - return 11223344556677; + return '2020-01-01T00:00:00.000Z'; }, })); @@ -53,11 +53,14 @@ jest.mock('../../lib/kibana', () => ({ }, }, }), + KibanaServices: { + get: jest.fn(() => ({ uiSettings: { get: () => ({ from: 'now-24h', to: 'now' }) } })), + }, })); describe('UrlStateContainer', () => { afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); describe('handleInitialize', () => { describe('URL state updates redux', () => { @@ -75,19 +78,19 @@ describe('UrlStateContainer', () => { mount( useUrlStateHooks(args)} />); expect(mockSetRelativeRangeDatePicker.mock.calls[1][0]).toEqual({ - from: 11223344556677, + from: '2020-01-01T00:00:00.000Z', fromStr: 'now-1d/d', kind: 'relative', - to: 11223344556677, + to: '2020-01-01T00:00:00.000Z', toStr: 'now-1d/d', id: 'global', }); expect(mockSetRelativeRangeDatePicker.mock.calls[0][0]).toEqual({ - from: 11223344556677, + from: '2020-01-01T00:00:00.000Z', fromStr: 'now-15m', kind: 'relative', - to: 11223344556677, + to: '2020-01-01T00:00:00.000Z', toStr: 'now', id: 'timeline', }); @@ -104,16 +107,16 @@ describe('UrlStateContainer', () => { mount( useUrlStateHooks(args)} />); expect(mockSetAbsoluteRangeDatePicker.mock.calls[1][0]).toEqual({ - from: 1556736012685, + from: '2019-05-01T18:40:12.685Z', kind: 'absolute', - to: 1556822416082, + to: '2019-05-02T18:40:16.082Z', id: 'global', }); expect(mockSetAbsoluteRangeDatePicker.mock.calls[0][0]).toEqual({ - from: 1556736012685, + from: '2019-05-01T18:40:12.685Z', kind: 'absolute', - to: 1556822416082, + to: '2019-05-02T18:40:16.082Z', id: 'timeline', }); } @@ -157,7 +160,7 @@ describe('UrlStateContainer', () => { ).toEqual({ hash: '', pathname: examplePath, - search: `?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))`, + search: `?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, state: '', }); } @@ -195,10 +198,10 @@ describe('UrlStateContainer', () => { if (CONSTANTS.detectionsPage === page) { expect(mockSetRelativeRangeDatePicker.mock.calls[3][0]).toEqual({ - from: 11223344556677, + from: '2020-01-01T00:00:00.000Z', fromStr: 'now-1d/d', kind: 'relative', - to: 11223344556677, + to: '2020-01-01T00:00:00.000Z', toStr: 'now-1d/d', id: 'global', }); diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx index f7502661da308..723f2d235864f 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx @@ -54,20 +54,20 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => [CONSTANTS.timerange]: { global: { [CONSTANTS.timerange]: { - from: 0, + from: '2020-07-07T08:20:18.966Z', fromStr: 'now-24h', kind: 'relative', - to: 1, + to: '2020-07-08T08:20:18.966Z', toStr: 'now', }, linkTo: ['timeline'], }, timeline: { [CONSTANTS.timerange]: { - from: 0, + from: '2020-07-07T08:20:18.966Z', fromStr: 'now-24h', kind: 'relative', - to: 1, + to: '2020-07-08T08:20:18.966Z', toStr: 'now', }, linkTo: ['global'], @@ -83,7 +83,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => hash: '', pathname: '/network', search: - "?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:0,fromStr:now-24h,kind:relative,to:1,toStr:now)),timeline:(linkTo:!(global),timerange:(from:0,fromStr:now-24h,kind:relative,to:1,toStr:now)))", + "?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", state: '', }); }); @@ -114,7 +114,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => hash: '', pathname: '/network', search: - "?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))", + "?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", state: '', }); }); @@ -147,7 +147,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => hash: '', pathname: '/network', search: - '?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))&timeline=(id:hello_timeline_id,isOpen:!t)', + "?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))&timeline=(id:hello_timeline_id,isOpen:!t)", state: '', }); }); @@ -176,7 +176,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => hash: '', pathname: examplePath, search: - '?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + "?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", state: '', }); } @@ -204,7 +204,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => expect( mockHistory.replace.mock.calls[mockHistory.replace.mock.calls.length - 1][0].search ).toEqual( - '?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))' + "?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))" ); wrapper.setProps({ hookProps: updatedProps }); @@ -213,7 +213,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => expect( mockHistory.replace.mock.calls[mockHistory.replace.mock.calls.length - 1][0].search ).toEqual( - "?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))" + "?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))" ); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx index ab03e2199474c..6eccf52ec72da 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx @@ -120,6 +120,7 @@ const updateTimerange = (newUrlStateString: string, dispatch: Dispatch) => { const absoluteRange = normalizeTimeRange( get('timeline.timerange', timerangeStateData) ); + dispatch( inputsActions.setAbsoluteRangeDatePicker({ ...absoluteRange, @@ -127,10 +128,12 @@ const updateTimerange = (newUrlStateString: string, dispatch: Dispatch) => { }) ); } + if (timelineType === 'relative') { const relativeRange = normalizeTimeRange( get('timeline.timerange', timerangeStateData) ); + dispatch( inputsActions.setRelativeRangeDatePicker({ ...relativeRange, @@ -145,6 +148,7 @@ const updateTimerange = (newUrlStateString: string, dispatch: Dispatch) => { const absoluteRange = normalizeTimeRange( get('global.timerange', timerangeStateData) ); + dispatch( inputsActions.setAbsoluteRangeDatePicker({ ...absoluteRange, @@ -156,6 +160,7 @@ const updateTimerange = (newUrlStateString: string, dispatch: Dispatch) => { const relativeRange = normalizeTimeRange( get('global.timerange', timerangeStateData) ); + dispatch( inputsActions.setRelativeRangeDatePicker({ ...relativeRange, diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/normalize_time_range.test.ts b/x-pack/plugins/security_solution/public/common/components/url_state/normalize_time_range.test.ts index dcdadf0f34072..d0cd9a2685077 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/normalize_time_range.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/normalize_time_range.test.ts @@ -13,8 +13,32 @@ import { isRelativeTimeRange, } from '../../store/inputs/model'; +import { getTimeRangeSettings } from '../../utils/default_date_settings'; + +const getTimeRangeSettingsMock = getTimeRangeSettings as jest.Mock; + +jest.mock('../../utils/default_date_settings'); +jest.mock('@elastic/datemath', () => ({ + parse: (date: string) => { + if (date === 'now') { + return { toISOString: () => '2020-07-08T08:20:18.966Z' }; + } + + if (date === 'now-24h') { + return { toISOString: () => '2020-07-07T08:20:18.966Z' }; + } + }, +})); + +getTimeRangeSettingsMock.mockImplementation(() => ({ + from: '2020-07-04T08:20:18.966Z', + to: '2020-07-05T08:20:18.966Z', + fromStr: 'now-24h', + toStr: 'now', +})); + describe('#normalizeTimeRange', () => { - test('Absolute time range returns empty strings as 0', () => { + test('Absolute time range returns defaults for empty strings', () => { const dateTimeRange: URLTimeRange = { kind: 'absolute', fromStr: undefined, @@ -25,30 +49,8 @@ describe('#normalizeTimeRange', () => { if (isAbsoluteTimeRange(dateTimeRange)) { const expected: AbsoluteTimeRange = { kind: 'absolute', - from: 0, - to: 0, - fromStr: undefined, - toStr: undefined, - }; - expect(normalizeTimeRange(dateTimeRange)).toEqual(expected); - } else { - throw new Error('Was expecting date time range to be a AbsoluteTimeRange'); - } - }); - - test('Absolute time range returns string with empty spaces as 0', () => { - const dateTimeRange: URLTimeRange = { - kind: 'absolute', - fromStr: undefined, - toStr: undefined, - from: ' ', - to: ' ', - }; - if (isAbsoluteTimeRange(dateTimeRange)) { - const expected: AbsoluteTimeRange = { - kind: 'absolute', - from: 0, - to: 0, + from: '2020-07-04T08:20:18.966Z', + to: '2020-07-05T08:20:18.966Z', fromStr: undefined, toStr: undefined, }; @@ -71,8 +73,8 @@ describe('#normalizeTimeRange', () => { if (isAbsoluteTimeRange(dateTimeRange)) { const expected: AbsoluteTimeRange = { kind: 'absolute', - from: from.valueOf(), - to: to.valueOf(), + from: from.toISOString(), + to: to.toISOString(), fromStr: undefined, toStr: undefined, }; @@ -89,14 +91,14 @@ describe('#normalizeTimeRange', () => { kind: 'absolute', fromStr: undefined, toStr: undefined, - from: from.valueOf(), - to: to.valueOf(), + from: from.toISOString(), + to: to.toISOString(), }; if (isAbsoluteTimeRange(dateTimeRange)) { const expected: AbsoluteTimeRange = { kind: 'absolute', - from: from.valueOf(), - to: to.valueOf(), + from: from.toISOString(), + to: to.toISOString(), fromStr: undefined, toStr: undefined, }; @@ -113,14 +115,14 @@ describe('#normalizeTimeRange', () => { kind: 'absolute', fromStr: undefined, toStr: undefined, - from: `${from.valueOf()}`, - to: `${to.valueOf()}`, + from: `${from.toISOString()}`, + to: `${to.toISOString()}`, }; if (isAbsoluteTimeRange(dateTimeRange)) { const expected: AbsoluteTimeRange = { kind: 'absolute', - from: from.valueOf(), - to: to.valueOf(), + from: from.toISOString(), + to: to.toISOString(), fromStr: undefined, toStr: undefined, }; @@ -130,7 +132,7 @@ describe('#normalizeTimeRange', () => { } }); - test('Absolute time range returns NaN with from and to when garbage is sent in', () => { + test('Absolute time range returns defaults when garbage is sent in', () => { const to = 'garbage'; const from = 'garbage'; const dateTimeRange: URLTimeRange = { @@ -143,8 +145,8 @@ describe('#normalizeTimeRange', () => { if (isAbsoluteTimeRange(dateTimeRange)) { const expected: AbsoluteTimeRange = { kind: 'absolute', - from: NaN, - to: NaN, + from: '2020-07-04T08:20:18.966Z', + to: '2020-07-05T08:20:18.966Z', fromStr: undefined, toStr: undefined, }; @@ -154,7 +156,7 @@ describe('#normalizeTimeRange', () => { } }); - test('Relative time range returns empty strings as 0', () => { + test('Relative time range returns defaults fro empty strings', () => { const dateTimeRange: URLTimeRange = { kind: 'relative', fromStr: '', @@ -165,30 +167,8 @@ describe('#normalizeTimeRange', () => { if (isRelativeTimeRange(dateTimeRange)) { const expected: RelativeTimeRange = { kind: 'relative', - from: 0, - to: 0, - fromStr: '', - toStr: '', - }; - expect(normalizeTimeRange(dateTimeRange)).toEqual(expected); - } else { - throw new Error('Was expecting date time range to be a RelativeTimeRange'); - } - }); - - test('Relative time range returns string with empty spaces as 0', () => { - const dateTimeRange: URLTimeRange = { - kind: 'relative', - fromStr: '', - toStr: '', - from: ' ', - to: ' ', - }; - if (isRelativeTimeRange(dateTimeRange)) { - const expected: RelativeTimeRange = { - kind: 'relative', - from: 0, - to: 0, + from: '2020-07-04T08:20:18.966Z', + to: '2020-07-05T08:20:18.966Z', fromStr: '', toStr: '', }; @@ -211,8 +191,8 @@ describe('#normalizeTimeRange', () => { if (isRelativeTimeRange(dateTimeRange)) { const expected: RelativeTimeRange = { kind: 'relative', - from: from.valueOf(), - to: to.valueOf(), + from: from.toISOString(), + to: to.toISOString(), fromStr: '', toStr: '', }; @@ -229,14 +209,14 @@ describe('#normalizeTimeRange', () => { kind: 'relative', fromStr: '', toStr: '', - from: from.valueOf(), - to: to.valueOf(), + from: from.toISOString(), + to: to.toISOString(), }; if (isRelativeTimeRange(dateTimeRange)) { const expected: RelativeTimeRange = { kind: 'relative', - from: from.valueOf(), - to: to.valueOf(), + from: from.toISOString(), + to: to.toISOString(), fromStr: '', toStr: '', }; @@ -253,14 +233,14 @@ describe('#normalizeTimeRange', () => { kind: 'relative', fromStr: '', toStr: '', - from: `${from.valueOf()}`, - to: `${to.valueOf()}`, + from: `${from.toISOString()}`, + to: `${to.toISOString()}`, }; if (isRelativeTimeRange(dateTimeRange)) { const expected: RelativeTimeRange = { kind: 'relative', - from: from.valueOf(), - to: to.valueOf(), + from: from.toISOString(), + to: to.toISOString(), fromStr: '', toStr: '', }; @@ -270,7 +250,7 @@ describe('#normalizeTimeRange', () => { } }); - test('Relative time range returns NaN with from and to when garbage is sent in', () => { + test('Relative time range returns defaults when garbage is sent in', () => { const to = 'garbage'; const from = 'garbage'; const dateTimeRange: URLTimeRange = { @@ -283,8 +263,8 @@ describe('#normalizeTimeRange', () => { if (isRelativeTimeRange(dateTimeRange)) { const expected: RelativeTimeRange = { kind: 'relative', - from: NaN, - to: NaN, + from: '2020-07-04T08:20:18.966Z', + to: '2020-07-05T08:20:18.966Z', fromStr: '', toStr: '', }; diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/normalize_time_range.ts b/x-pack/plugins/security_solution/public/common/components/url_state/normalize_time_range.ts index 851f89dcd2a5a..6dc0949665530 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/normalize_time_range.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/normalize_time_range.ts @@ -5,13 +5,20 @@ */ import { URLTimeRange } from '../../store/inputs/model'; +import { getTimeRangeSettings } from '../../utils/default_date_settings'; import { getMaybeDate } from '../formatted_date/maybe_date'; -export const normalizeTimeRange = (dateRange: T): T => { +export const normalizeTimeRange = < + T extends URLTimeRange | { to: string | number; from: string | number } +>( + dateRange: T, + uiSettings = true +): T => { const maybeTo = getMaybeDate(dateRange.to); const maybeFrom = getMaybeDate(dateRange.from); - const to: number = maybeTo.isValid() ? maybeTo.valueOf() : Number(dateRange.to); - const from: number = maybeFrom.isValid() ? maybeFrom.valueOf() : Number(dateRange.from); + const { to: benchTo, from: benchFrom } = getTimeRangeSettings(uiSettings); + const to: string = maybeTo.isValid() ? maybeTo.toISOString() : benchTo; + const from: string = maybeFrom.isValid() ? maybeFrom.toISOString() : benchFrom; return { ...dateRange, to, diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts b/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts index dec1672b076eb..8d471e843320c 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts @@ -92,20 +92,20 @@ export const defaultProps: UrlStateContainerPropTypes = { [CONSTANTS.timerange]: { global: { [CONSTANTS.timerange]: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, linkTo: ['timeline'], }, timeline: { [CONSTANTS.timerange]: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, linkTo: ['global'], diff --git a/x-pack/plugins/security_solution/public/common/components/utils.ts b/x-pack/plugins/security_solution/public/common/components/utils.ts index ff022fd7d763d..3620b09495eb6 100644 --- a/x-pack/plugins/security_solution/public/common/components/utils.ts +++ b/x-pack/plugins/security_solution/public/common/components/utils.ts @@ -20,7 +20,7 @@ export const getDaysDiff = (minDate: moment.Moment, maxDate: moment.Moment) => { return diff; }; -export const histogramDateTimeFormatter = (domain: [number, number] | null, fixedDiff?: number) => { +export const histogramDateTimeFormatter = (domain: [string, string] | null, fixedDiff?: number) => { const diff = fixedDiff ?? getDaysDiff(moment(domain![0]), moment(domain![1])); const format = niceTimeFormatByDay(diff); return timeFormatter(format); diff --git a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts index 6050dafc0b191..00b78c3a96550 100644 --- a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts @@ -19,6 +19,7 @@ import { useUiSetting$ } from '../../../lib/kibana'; import { LastEventTimeGqlQuery } from './last_event_time.gql_query'; import { useApolloClient } from '../../../utils/apollo_context'; +import { useWithSource } from '../../source'; export interface LastEventTimeArgs { id: string; @@ -44,6 +45,8 @@ export function useLastEventTimeQuery( const [currentIndexKey, updateCurrentIndexKey] = useState(null); const [defaultIndex] = useUiSetting$(DEFAULT_INDEX_KEY); const apolloClient = useApolloClient(); + const { docValueFields } = useWithSource(sourceId); + async function fetchLastEventTime(signal: AbortSignal) { updateLoading(true); if (apolloClient) { @@ -52,6 +55,7 @@ export function useLastEventTimeQuery( query: LastEventTimeGqlQuery, fetchPolicy: 'cache-first', variables: { + docValueFields, sourceId, indexKey, details, diff --git a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/last_event_time.gql_query.ts b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/last_event_time.gql_query.ts index 049c73b607b7e..36305ef0dc882 100644 --- a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/last_event_time.gql_query.ts +++ b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/last_event_time.gql_query.ts @@ -12,10 +12,16 @@ export const LastEventTimeGqlQuery = gql` $indexKey: LastEventIndexKey! $details: LastTimeDetails! $defaultIndex: [String!]! + $docValueFields: [docValueFieldsInput!]! ) { source(id: $sourceId) { id - LastEventTime(indexKey: $indexKey, details: $details, defaultIndex: $defaultIndex) { + LastEventTime( + indexKey: $indexKey + details: $details + defaultIndex: $defaultIndex + docValueFields: $docValueFields + ) { lastSeen } } diff --git a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/mock.ts b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/mock.ts index 938473f92782a..bdeb1db4e1b28 100644 --- a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/mock.ts +++ b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/mock.ts @@ -44,6 +44,7 @@ export const mockLastEventTimeQuery: MockLastEventTimeQuery[] = [ indexKey: LastEventIndexKey.hosts, details: {}, defaultIndex: DEFAULT_INDEX_PATTERN, + docValueFields: [], }, }, result: { diff --git a/x-pack/plugins/security_solution/public/common/containers/global_time/index.tsx b/x-pack/plugins/security_solution/public/common/containers/global_time/index.tsx new file mode 100644 index 0000000000000..f2545c1642d49 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/global_time/index.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useState, useEffect } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; + +import { inputsModel, inputsSelectors, State } from '../../store'; +import { inputsActions } from '../../store/actions'; + +interface SetQuery { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch | inputsModel.RefetchKql; +} + +export interface GlobalTimeArgs { + from: string; + to: string; + setQuery: ({ id, inspect, loading, refetch }: SetQuery) => void; + deleteQuery?: ({ id }: { id: string }) => void; + isInitializing: boolean; +} + +interface OwnProps { + children: (args: GlobalTimeArgs) => React.ReactNode; +} + +type GlobalTimeProps = OwnProps & PropsFromRedux; + +export const GlobalTimeComponent: React.FC = ({ + children, + deleteAllQuery, + deleteOneQuery, + from, + to, + setGlobalQuery, +}) => { + const [isInitializing, setIsInitializing] = useState(true); + + const setQuery = useCallback( + ({ id, inspect, loading, refetch }: SetQuery) => + setGlobalQuery({ inputId: 'global', id, inspect, loading, refetch }), + [setGlobalQuery] + ); + + const deleteQuery = useCallback( + ({ id }: { id: string }) => deleteOneQuery({ inputId: 'global', id }), + [deleteOneQuery] + ); + + useEffect(() => { + if (isInitializing) { + setIsInitializing(false); + } + return () => { + deleteAllQuery({ id: 'global' }); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + <> + {children({ + isInitializing, + from, + to, + setQuery, + deleteQuery, + })} + + ); +}; + +const mapStateToProps = (state: State) => { + const timerange: inputsModel.TimeRange = inputsSelectors.globalTimeRangeSelector(state); + return { + from: timerange.from, + to: timerange.to, + }; +}; + +const mapDispatchToProps = { + deleteAllQuery: inputsActions.deleteAllQuery, + deleteOneQuery: inputsActions.deleteOneQuery, + setGlobalQuery: inputsActions.setQuery, +}; + +export const connector = connect(mapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const GlobalTime = connector(React.memo(GlobalTimeComponent)); + +GlobalTime.displayName = 'GlobalTime'; diff --git a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.test.tsx index cb988d7ebf190..6e780e6b06b52 100644 --- a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.test.tsx @@ -61,13 +61,13 @@ describe('useQuery', () => { }); const TestComponent = () => { result = useQuery({ - endDate: 100, + endDate: '2020-07-07T08:20:00.000Z', errorMessage: 'fakeErrorMsg', filterQuery: '', histogramType: HistogramType.alerts, isInspected: false, stackByField: 'fakeField', - startDate: 0, + startDate: '2020-07-07T08:08:00.000Z', }); return
; @@ -85,8 +85,8 @@ describe('useQuery', () => { sourceId: 'default', timerange: { interval: '12h', - from: 0, - to: 100, + from: '2020-07-07T08:08:00.000Z', + to: '2020-07-07T08:20:00.000Z', }, defaultIndex: 'mockDefaultIndex', inspect: false, @@ -123,13 +123,13 @@ describe('useQuery', () => { }); const TestComponent = () => { result = useQuery({ - endDate: 100, + endDate: '2020-07-07T08:20:18.966Z', errorMessage: 'fakeErrorMsg', filterQuery: '', histogramType: HistogramType.alerts, isInspected: false, stackByField: 'fakeField', - startDate: 0, + startDate: '2020-07-08T08:20:18.966Z', }); return
; diff --git a/x-pack/plugins/security_solution/public/common/containers/query_template.tsx b/x-pack/plugins/security_solution/public/common/containers/query_template.tsx index fdc95c1dadfe1..eaa43c255a944 100644 --- a/x-pack/plugins/security_solution/public/common/containers/query_template.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/query_template.tsx @@ -9,14 +9,18 @@ import React from 'react'; import { FetchMoreOptions, FetchMoreQueryOptions, OperationVariables } from 'react-apollo'; import { ESQuery } from '../../../common/typed_json'; +import { DocValueFields } from './source'; + +export { DocValueFields }; export interface QueryTemplateProps { + docValueFields?: DocValueFields[]; id?: string; - endDate?: number; + endDate?: string; filterQuery?: ESQuery | string; skip?: boolean; sourceId: string; - startDate?: number; + startDate?: string; } // eslint-disable-next-line @typescript-eslint/no-explicit-any export type FetchMoreOptionsArgs = FetchMoreQueryOptions & diff --git a/x-pack/plugins/security_solution/public/common/containers/query_template_paginated.tsx b/x-pack/plugins/security_solution/public/common/containers/query_template_paginated.tsx index 446e1125b2807..f40ae4d31c586 100644 --- a/x-pack/plugins/security_solution/public/common/containers/query_template_paginated.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/query_template_paginated.tsx @@ -13,14 +13,18 @@ import deepEqual from 'fast-deep-equal'; import { ESQuery } from '../../../common/typed_json'; import { inputsModel } from '../store/model'; import { generateTablePaginationOptions } from '../components/paginated_table/helpers'; +import { DocValueFields } from './source'; + +export { DocValueFields }; export interface QueryTemplatePaginatedProps { + docValueFields?: DocValueFields[]; id?: string; - endDate?: number; + endDate?: string; filterQuery?: ESQuery | string; skip?: boolean; sourceId: string; - startDate?: number; + startDate?: string; } // eslint-disable-next-line @typescript-eslint/no-explicit-any type FetchMoreOptionsArgs = FetchMoreQueryOptions & diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx index bfde17723aef4..03ad6ad3396f8 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx @@ -25,6 +25,7 @@ describe('Index Fields & Browser Fields', () => { return expect(initialResult).toEqual({ browserFields: {}, + docValueFields: [], errorMessage: null, indexPattern: { fields: [], @@ -56,6 +57,16 @@ describe('Index Fields & Browser Fields', () => { current: { indicesExist: true, browserFields: mockBrowserFields, + docValueFields: [ + { + field: '@timestamp', + format: 'date_time', + }, + { + field: 'event.end', + format: 'date_time', + }, + ], indexPattern: { fields: mockIndexFields, title: diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx index 4f42f20c45ae1..9b7dfe84277c6 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx @@ -33,6 +33,11 @@ export interface BrowserField { type: string; } +export interface DocValueFields { + field: string; + format: string; +} + export type BrowserFields = Readonly>>; export const getAllBrowserFields = (browserFields: BrowserFields): Array> => @@ -75,14 +80,38 @@ export const getBrowserFields = memoizeOne( (newArgs, lastArgs) => newArgs[0] === lastArgs[0] ); +export const getdocValueFields = memoizeOne( + (_title: string, fields: IndexField[]): DocValueFields[] => + fields && fields.length > 0 + ? fields.reduce((accumulator: DocValueFields[], field: IndexField) => { + if (field.type === 'date' && accumulator.length < 100) { + const format: string = + field.format != null && !isEmpty(field.format) ? field.format : 'date_time'; + return [ + ...accumulator, + { + field: field.name, + format, + }, + ]; + } + return accumulator; + }, []) + : [], + // Update the value only if _title has changed + (newArgs, lastArgs) => newArgs[0] === lastArgs[0] +); + export const indicesExistOrDataTemporarilyUnavailable = ( indicesExist: boolean | null | undefined ) => indicesExist || isUndefined(indicesExist); const EMPTY_BROWSER_FIELDS = {}; +const EMPTY_DOCVALUE_FIELD: DocValueFields[] = []; interface UseWithSourceState { browserFields: BrowserFields; + docValueFields: DocValueFields[]; errorMessage: string | null; indexPattern: IIndexPattern; indicesExist: boolean | undefined | null; @@ -104,6 +133,7 @@ export const useWithSource = ( const [state, setState] = useState({ browserFields: EMPTY_BROWSER_FIELDS, + docValueFields: EMPTY_DOCVALUE_FIELD, errorMessage: null, indexPattern: getIndexFields(defaultIndex.join(), []), indicesExist: indicesExistOrDataTemporarilyUnavailable(undefined), @@ -146,6 +176,10 @@ export const useWithSource = ( defaultIndex.join(), get('data.source.status.indexFields', result) ), + docValueFields: getdocValueFields( + defaultIndex.join(), + get('data.source.status.indexFields', result) + ), indexPattern: getIndexFields( defaultIndex.join(), get('data.source.status.indexFields', result) diff --git a/x-pack/plugins/security_solution/public/common/containers/source/mock.ts b/x-pack/plugins/security_solution/public/common/containers/source/mock.ts index 55e8b6ac02b12..bba6a15d73970 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/mock.ts +++ b/x-pack/plugins/security_solution/public/common/containers/source/mock.ts @@ -6,7 +6,7 @@ import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; -import { BrowserFields } from '.'; +import { BrowserFields, DocValueFields } from '.'; import { sourceQuery } from './index.gql_query'; export const mocksSource = [ @@ -697,3 +697,14 @@ export const mockBrowserFields: BrowserFields = { }, }, }; + +export const mockDocValueFields: DocValueFields[] = [ + { + field: '@timestamp', + format: 'date_time', + }, + { + field: 'event.end', + format: 'date_time', + }, +]; diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index 89f100992e1b9..2849e8ffabd36 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -156,7 +156,13 @@ export const mockGlobalState: State = { }, inputs: { global: { - timerange: { kind: 'relative', fromStr: DEFAULT_FROM, toStr: DEFAULT_TO, from: 0, to: 1 }, + timerange: { + kind: 'relative', + fromStr: DEFAULT_FROM, + toStr: DEFAULT_TO, + from: '2020-07-07T08:20:18.966Z', + to: '2020-07-08T08:20:18.966Z', + }, linkTo: ['timeline'], queries: [], policy: { kind: DEFAULT_INTERVAL_TYPE, duration: DEFAULT_INTERVAL_VALUE }, @@ -167,7 +173,13 @@ export const mockGlobalState: State = { filters: [], }, timeline: { - timerange: { kind: 'relative', fromStr: DEFAULT_FROM, toStr: DEFAULT_TO, from: 0, to: 1 }, + timerange: { + kind: 'relative', + fromStr: DEFAULT_FROM, + toStr: DEFAULT_TO, + from: '2020-07-07T08:20:18.966Z', + to: '2020-07-08T08:20:18.966Z', + }, linkTo: ['global'], queries: [], policy: { kind: DEFAULT_INTERVAL_TYPE, duration: DEFAULT_INTERVAL_VALUE }, @@ -211,8 +223,8 @@ export const mockGlobalState: State = { templateTimelineVersion: null, noteIds: [], dateRange: { - start: 0, - end: 0, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', }, selectedEventIds: {}, show: false, diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index b1df41a19aebe..a415ab75f13ea 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -2091,8 +2091,8 @@ export const mockTimelineModel: TimelineModel = { ], dataProviders: [], dateRange: { - end: 1584539558929, - start: 1584539198929, + end: '2020-03-18T13:52:38.929Z', + start: '2020-03-18T13:46:38.929Z', }, deletedEventIds: [], description: 'This is a sample rule description', @@ -2154,7 +2154,7 @@ export const mockTimelineModel: TimelineModel = { export const mockTimelineResult: TimelineResult = { savedObjectId: 'ef579e40-jibber-jabber', columns: timelineDefaults.columns.filter((column) => column.id !== 'event.action'), - dateRange: { start: 1584539198929, end: 1584539558929 }, + dateRange: { start: '2020-03-18T13:46:38.929Z', end: '2020-03-18T13:52:38.929Z' }, description: 'This is a sample rule description', eventType: 'all', filters: [ @@ -2188,7 +2188,7 @@ export const mockTimelineApolloResult = { }; export const defaultTimelineProps: CreateTimelineProps = { - from: 1541444305937, + from: '2018-11-05T18:58:25.937Z', timeline: { columns: [ { columnHeaderType: 'not-filtered', id: '@timestamp', width: 190 }, @@ -2212,7 +2212,7 @@ export const defaultTimelineProps: CreateTimelineProps = { queryMatch: { field: '_id', operator: ':', value: '1' }, }, ], - dateRange: { end: 1541444605937, start: 1541444305937 }, + dateRange: { end: '2018-11-05T19:03:25.937Z', start: '2018-11-05T18:58:25.937Z' }, deletedEventIds: [], description: '', eventIdToNoteIds: {}, @@ -2251,6 +2251,6 @@ export const defaultTimelineProps: CreateTimelineProps = { version: null, width: 1100, }, - to: 1541444605937, + to: '2018-11-05T19:03:25.937Z', ruleNote: '# this is some markdown documentation', }; diff --git a/x-pack/plugins/security_solution/public/common/store/inputs/actions.ts b/x-pack/plugins/security_solution/public/common/store/inputs/actions.ts index f8b8d0865d120..efad0638b2971 100644 --- a/x-pack/plugins/security_solution/public/common/store/inputs/actions.ts +++ b/x-pack/plugins/security_solution/public/common/store/inputs/actions.ts @@ -14,21 +14,21 @@ const actionCreator = actionCreatorFactory('x-pack/security_solution/local/input export const setAbsoluteRangeDatePicker = actionCreator<{ id: InputsModelId; - from: number; - to: number; + from: string; + to: string; }>('SET_ABSOLUTE_RANGE_DATE_PICKER'); export const setTimelineRangeDatePicker = actionCreator<{ - from: number; - to: number; + from: string; + to: string; }>('SET_TIMELINE_RANGE_DATE_PICKER'); export const setRelativeRangeDatePicker = actionCreator<{ id: InputsModelId; fromStr: string; toStr: string; - from: number; - to: number; + from: string; + to: string; }>('SET_RELATIVE_RANGE_DATE_PICKER'); export const setDuration = actionCreator<{ id: InputsModelId; duration: number }>('SET_DURATION'); diff --git a/x-pack/plugins/security_solution/public/common/store/inputs/helpers.test.ts b/x-pack/plugins/security_solution/public/common/store/inputs/helpers.test.ts index d23110b44ad43..b54d8ca20b0d1 100644 --- a/x-pack/plugins/security_solution/public/common/store/inputs/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/common/store/inputs/helpers.test.ts @@ -53,8 +53,8 @@ describe('Inputs', () => { kind: 'relative', fromStr: 'now-48h', toStr: 'now', - from: 23, - to: 26, + from: '2020-07-06T08:00:00.000Z', + to: '2020-07-08T08:00:00.000Z', }; const newState: InputsModel = updateInputTimerange('global', newTimerange, state); expect(newState.timeline.timerange).toEqual(newState.global.timerange); @@ -65,8 +65,8 @@ describe('Inputs', () => { kind: 'relative', fromStr: 'now-68h', toStr: 'NOTnow', - from: 29, - to: 33, + from: '2020-07-05T22:00:00.000Z', + to: '2020-07-08T18:00:00.000Z', }; const newState: InputsModel = updateInputTimerange('timeline', newTimerange, state); expect(newState.timeline.timerange).toEqual(newState.global.timerange); @@ -83,8 +83,8 @@ describe('Inputs', () => { kind: 'relative', fromStr: 'now-48h', toStr: 'now', - from: 23, - to: 26, + from: '2020-07-06T08:00:00.000Z', + to: '2020-07-08T08:00:00.000Z', }; const newState: InputsModel = updateInputTimerange('global', newTimerange, state); expect(newState.timeline.timerange).toEqual(state.timeline.timerange); @@ -96,8 +96,8 @@ describe('Inputs', () => { kind: 'relative', fromStr: 'now-68h', toStr: 'NOTnow', - from: 29, - to: 33, + from: '2020-07-05T22:00:00.000Z', + to: '2020-07-08T18:00:00.000Z', }; const newState: InputsModel = updateInputTimerange('timeline', newTimerange, state); expect(newState.timeline.timerange).toEqual(newTimerange); @@ -274,10 +274,10 @@ describe('Inputs', () => { }, ], timerange: { - from: 0, + from: '2020-07-07T08:20:18.966Z', fromStr: 'now-24h', kind: 'relative', - to: 1, + to: '2020-07-08T08:20:18.966Z', toStr: 'now', }, query: { query: '', language: 'kuery' }, @@ -291,10 +291,10 @@ describe('Inputs', () => { }, queries: [], timerange: { - from: 0, + from: '2020-07-07T08:20:18.966Z', fromStr: 'now-24h', kind: 'relative', - to: 1, + to: '2020-07-08T08:20:18.966Z', toStr: 'now', }, query: { query: '', language: 'kuery' }, diff --git a/x-pack/plugins/security_solution/public/common/store/inputs/model.ts b/x-pack/plugins/security_solution/public/common/store/inputs/model.ts index e851caf523eb4..358124405c146 100644 --- a/x-pack/plugins/security_solution/public/common/store/inputs/model.ts +++ b/x-pack/plugins/security_solution/public/common/store/inputs/model.ts @@ -13,16 +13,16 @@ export interface AbsoluteTimeRange { kind: 'absolute'; fromStr: undefined; toStr: undefined; - from: number; - to: number; + from: string; + to: string; } export interface RelativeTimeRange { kind: 'relative'; fromStr: string; toStr: string; - from: number; - to: number; + from: string; + to: string; } export const isRelativeTimeRange = ( @@ -35,10 +35,7 @@ export const isAbsoluteTimeRange = ( export type TimeRange = AbsoluteTimeRange | RelativeTimeRange; -export type URLTimeRange = Omit & { - from: string | TimeRange['from']; - to: string | TimeRange['to']; -}; +export type URLTimeRange = TimeRange; export interface Policy { kind: 'manual' | 'interval'; diff --git a/x-pack/plugins/security_solution/public/common/utils/default_date_settings.test.ts b/x-pack/plugins/security_solution/public/common/utils/default_date_settings.test.ts index 9fc5490b16cab..c0e009c46a6b6 100644 --- a/x-pack/plugins/security_solution/public/common/utils/default_date_settings.test.ts +++ b/x-pack/plugins/security_solution/public/common/utils/default_date_settings.test.ts @@ -217,38 +217,38 @@ describe('getTimeRangeSettings', () => { test('should return DEFAULT_FROM', () => { mockTimeRange(); const { from } = getTimeRangeSettings(); - expect(from).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + expect(from).toBe(new Date(DEFAULT_FROM_DATE).toISOString()); }); test('should return a custom from range', () => { const mockFrom = '2019-08-30T17:49:18.396Z'; mockTimeRange({ from: mockFrom }); const { from } = getTimeRangeSettings(); - expect(from).toBe(new Date(mockFrom).valueOf()); + expect(from).toBe(new Date(mockFrom).toISOString()); }); test('should return the DEFAULT_FROM when the whole object is null', () => { mockTimeRange(null); const { from } = getTimeRangeSettings(); - expect(from).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + expect(from).toBe(new Date(DEFAULT_FROM_DATE).toISOString()); }); test('should return the DEFAULT_FROM when the whole object is undefined', () => { mockTimeRange(null); const { from } = getTimeRangeSettings(); - expect(from).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + expect(from).toBe(new Date(DEFAULT_FROM_DATE).toISOString()); }); test('should return the DEFAULT_FROM when the from value is null', () => { mockTimeRange({ from: null }); const { from } = getTimeRangeSettings(); - expect(from).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + expect(from).toBe(new Date(DEFAULT_FROM_DATE).toISOString()); }); test('should return the DEFAULT_FROM when the from value is undefined', () => { mockTimeRange({ from: undefined }); const { from } = getTimeRangeSettings(); - expect(from).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + expect(from).toBe(new Date(DEFAULT_FROM_DATE).toISOString()); }); test('should return the DEFAULT_FROM when the from value is malformed', () => { @@ -256,7 +256,7 @@ describe('getTimeRangeSettings', () => { if (isMalformedTimeRange(malformedTimeRange)) { mockTimeRange(malformedTimeRange); const { from } = getTimeRangeSettings(); - expect(from).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + expect(from).toBe(new Date(DEFAULT_FROM_DATE).toISOString()); } else { throw Error('Was expecting an object to be used for the malformed time range'); } @@ -271,7 +271,7 @@ describe('getTimeRangeSettings', () => { it('is DEFAULT_FROM in epoch', () => { const { from } = getTimeRangeSettings(false); - expect(from).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + expect(from).toBe(new Date(DEFAULT_FROM_DATE).toISOString()); }); }); }); @@ -280,38 +280,38 @@ describe('getTimeRangeSettings', () => { test('should return DEFAULT_TO', () => { mockTimeRange(); const { to } = getTimeRangeSettings(); - expect(to).toBe(new Date(DEFAULT_TO_DATE).valueOf()); + expect(to).toBe(new Date(DEFAULT_TO_DATE).toISOString()); }); test('should return a custom from range', () => { const mockTo = '2000-08-30T17:49:18.396Z'; mockTimeRange({ to: mockTo }); const { to } = getTimeRangeSettings(); - expect(to).toBe(new Date(mockTo).valueOf()); + expect(to).toBe(new Date(mockTo).toISOString()); }); test('should return the DEFAULT_TO_DATE when the whole object is null', () => { mockTimeRange(null); const { to } = getTimeRangeSettings(); - expect(to).toBe(new Date(DEFAULT_TO_DATE).valueOf()); + expect(to).toBe(new Date(DEFAULT_TO_DATE).toISOString()); }); test('should return the DEFAULT_TO_DATE when the whole object is undefined', () => { mockTimeRange(null); const { to } = getTimeRangeSettings(); - expect(to).toBe(new Date(DEFAULT_TO_DATE).valueOf()); + expect(to).toBe(new Date(DEFAULT_TO_DATE).toISOString()); }); test('should return the DEFAULT_TO_DATE when the from value is null', () => { mockTimeRange({ from: null }); const { to } = getTimeRangeSettings(); - expect(to).toBe(new Date(DEFAULT_TO_DATE).valueOf()); + expect(to).toBe(new Date(DEFAULT_TO_DATE).toISOString()); }); test('should return the DEFAULT_TO_DATE when the from value is undefined', () => { mockTimeRange({ from: undefined }); const { to } = getTimeRangeSettings(); - expect(to).toBe(new Date(DEFAULT_TO_DATE).valueOf()); + expect(to).toBe(new Date(DEFAULT_TO_DATE).toISOString()); }); test('should return the DEFAULT_TO_DATE when the from value is malformed', () => { @@ -319,7 +319,7 @@ describe('getTimeRangeSettings', () => { if (isMalformedTimeRange(malformedTimeRange)) { mockTimeRange(malformedTimeRange); const { to } = getTimeRangeSettings(); - expect(to).toBe(new Date(DEFAULT_TO_DATE).valueOf()); + expect(to).toBe(new Date(DEFAULT_TO_DATE).toISOString()); } else { throw Error('Was expecting an object to be used for the malformed time range'); } @@ -334,7 +334,7 @@ describe('getTimeRangeSettings', () => { it('is DEFAULT_TO in epoch', () => { const { to } = getTimeRangeSettings(false); - expect(to).toBe(new Date(DEFAULT_TO_DATE).valueOf()); + expect(to).toBe(new Date(DEFAULT_TO_DATE).toISOString()); }); }); }); @@ -498,12 +498,12 @@ describe('getIntervalSettings', () => { '1930-05-31T13:03:54.234Z', moment('1950-05-31T13:03:54.234Z') ); - expect(value.valueOf()).toBe(new Date('1930-05-31T13:03:54.234Z').valueOf()); + expect(value.toISOString()).toBe(new Date('1930-05-31T13:03:54.234Z').toISOString()); }); test('should return the second value if the first is a bad string', () => { const value = parseDateWithDefault('trashed string', moment('1950-05-31T13:03:54.234Z')); - expect(value.valueOf()).toBe(new Date('1950-05-31T13:03:54.234Z').valueOf()); + expect(value.toISOString()).toBe(new Date('1950-05-31T13:03:54.234Z').toISOString()); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/utils/default_date_settings.ts b/x-pack/plugins/security_solution/public/common/utils/default_date_settings.ts index b8b4b23e20b85..148143bb00bea 100644 --- a/x-pack/plugins/security_solution/public/common/utils/default_date_settings.ts +++ b/x-pack/plugins/security_solution/public/common/utils/default_date_settings.ts @@ -49,8 +49,8 @@ export const getTimeRangeSettings = (uiSettings = true) => { const fromStr = (isString(timeRange?.from) && timeRange?.from) || DEFAULT_FROM; const toStr = (isString(timeRange?.to) && timeRange?.to) || DEFAULT_TO; - const from = parseDateWithDefault(fromStr, DEFAULT_FROM_MOMENT).valueOf(); - const to = parseDateWithDefault(toStr, DEFAULT_TO_MOMENT).valueOf(); + const from = parseDateWithDefault(fromStr, DEFAULT_FROM_MOMENT).toISOString(); + const to = parseDateWithDefault(toStr, DEFAULT_TO_MOMENT).toISOString(); return { from, fromStr, to, toStr }; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.test.tsx index 7f340b0bea37b..09883e342f998 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.test.tsx @@ -18,8 +18,8 @@ describe('AlertsHistogram', () => { legendItems={[]} loading={false} data={[]} - from={0} - to={1} + from={'2020-07-07T08:20:18.966Z'} + to={'2020-07-08T08:20:18.966Z'} updateDateRange={jest.fn()} /> ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.tsx index 11dcbfa39d574..ffd7f7918ec72 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.tsx @@ -26,11 +26,11 @@ const DEFAULT_CHART_HEIGHT = 174; interface AlertsHistogramProps { chartHeight?: number; - from: number; + from: string; legendItems: LegendItem[]; legendPosition?: Position; loading: boolean; - to: number; + to: string; data: HistogramData[]; updateDateRange: UpdateDateRange; } diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.tsx index 9d124201f022e..0cbed86f18768 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.tsx @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment'; import { showAllOthersBucket } from '../../../../common/constants'; import { HistogramData, AlertsAggregation, AlertsBucket, AlertsGroupBucket } from './types'; @@ -28,8 +29,8 @@ export const formatAlertsData = (alertsData: AlertSearchResponse<{}, AlertsAggre export const getAlertsHistogramQuery = ( stackByField: string, - from: number, - to: number, + from: string, + to: string, additionalFilters: Array<{ bool: { filter: unknown[]; should: unknown[]; must_not: unknown[]; must: unknown[] }; }> @@ -55,7 +56,7 @@ export const getAlertsHistogramQuery = ( alerts: { date_histogram: { field: '@timestamp', - fixed_interval: `${Math.floor((to - from) / 32)}ms`, + fixed_interval: `${Math.floor(moment(to).diff(moment(from)) / 32)}ms`, min_doc_count: 0, extended_bounds: { min: from, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx index 59d97480418b7..4cbfa59aac582 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx @@ -40,10 +40,10 @@ jest.mock('../../../common/components/navigation/use_get_url_search'); describe('AlertsHistogramPanel', () => { const defaultProps = { - from: 0, + from: '2020-07-07T08:20:18.966Z', signalIndexName: 'signalIndexName', setQuery: jest.fn(), - to: 1, + to: '2020-07-08T08:20:18.966Z', updateDateRange: jest.fn(), }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 24bfeaa4dae1a..16d1a1481bc96 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -70,7 +70,7 @@ describe('alert actions', () => { updateTimelineIsLoading, }); const expected = { - from: 1541444305937, + from: '2018-11-05T18:58:25.937Z', timeline: { columns: [ { @@ -153,8 +153,8 @@ describe('alert actions', () => { ], dataProviders: [], dateRange: { - end: 1541444605937, - start: 1541444305937, + end: '2018-11-05T19:03:25.937Z', + start: '2018-11-05T18:58:25.937Z', }, deletedEventIds: [], description: 'This is a sample rule description', @@ -225,7 +225,7 @@ describe('alert actions', () => { version: null, width: 1100, }, - to: 1541444605937, + to: '2018-11-05T19:03:25.937Z', ruleNote: '# this is some markdown documentation', }; @@ -375,8 +375,8 @@ describe('alert actions', () => { }; const result = determineToAndFrom({ ecsData: ecsDataMock }); - expect(result.from).toEqual(1584726886349); - expect(result.to).toEqual(1584727186349); + expect(result.from).toEqual('2020-03-20T17:54:46.349Z'); + expect(result.to).toEqual('2020-03-20T17:59:46.349Z'); }); test('it uses current time timestamp if ecsData.timestamp is not provided', () => { @@ -385,8 +385,8 @@ describe('alert actions', () => { }; const result = determineToAndFrom({ ecsData: ecsDataMock }); - expect(result.from).toEqual(1583085286349); - expect(result.to).toEqual(1583085586349); + expect(result.from).toEqual('2020-03-01T17:54:46.349Z'); + expect(result.to).toEqual('2020-03-01T17:59:46.349Z'); }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index 11c13c2358e94..7bebc9efbee15 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -97,8 +97,8 @@ export const determineToAndFrom = ({ ecsData }: { ecsData: Ecs }) => { const from = moment(ecsData.timestamp ?? new Date()) .subtract(ellapsedTimeRule) - .valueOf(); - const to = moment(ecsData.timestamp ?? new Date()).valueOf(); + .toISOString(); + const to = moment(ecsData.timestamp ?? new Date()).toISOString(); return { to, from }; }; 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 index f99a0256c0b3f..563f2ea60cded 100644 --- 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 @@ -19,10 +19,10 @@ describe('AlertsTableComponent', () => { timelineId={TimelineId.test} canUserCRUD hasIndexWrite - from={0} + from={'2020-07-07T08:20:18.966Z'} loading signalsIndex="index" - to={1} + to={'2020-07-08T08:20:18.966Z'} globalQuery={{ query: 'query', language: 'language', diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index b9b963a84e966..391598ebda03d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -62,10 +62,10 @@ interface OwnProps { canUserCRUD: boolean; defaultFilters?: Filter[]; hasIndexWrite: boolean; - from: number; + from: string; loading: boolean; signalsIndex: string; - to: number; + to: string; } type AlertsTableComponentProps = OwnProps & PropsFromRedux; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts index 34d18b4dedba6..ebf1a6d3ed533 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts @@ -60,9 +60,9 @@ export interface SendAlertToTimelineActionProps { export type UpdateTimelineLoading = ({ id, isLoading }: { id: string; isLoading: boolean }) => void; export interface CreateTimelineProps { - from: number; + from: string; timeline: TimelineModel; - to: number; + to: string; ruleNote?: string; } diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.test.tsx index 0204a2980b9fc..d36c19a6a35c6 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.test.tsx @@ -374,6 +374,16 @@ describe('useFetchIndexPatterns', () => { 'winlogbeat-*', ], indicesExists: true, + docValueFields: [ + { + field: '@timestamp', + format: 'date_time', + }, + { + field: 'event.end', + format: 'date_time', + }, + ], indexPatterns: { fields: [ { name: '@timestamp', searchable: true, type: 'date', aggregatable: true }, @@ -441,6 +451,7 @@ describe('useFetchIndexPatterns', () => { expect(result.current).toEqual([ { browserFields: {}, + docValueFields: [], indexPatterns: { fields: [], title: '', diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx index 640d6f9a17fd1..ab12f045cddbc 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx @@ -12,8 +12,10 @@ import { IIndexPattern } from '../../../../../../../../src/plugins/data/public'; import { BrowserFields, getBrowserFields, + getdocValueFields, getIndexFields, sourceQuery, + DocValueFields, } from '../../../../common/containers/source'; import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; import { SourceQuery } from '../../../../graphql/types'; @@ -23,6 +25,7 @@ import * as i18n from './translations'; interface FetchIndexPatternReturn { browserFields: BrowserFields; + docValueFields: DocValueFields[]; isLoading: boolean; indices: string[]; indicesExists: boolean; @@ -31,18 +34,29 @@ interface FetchIndexPatternReturn { export type Return = [FetchIndexPatternReturn, Dispatch>]; +const DEFAULT_BROWSER_FIELDS = {}; +const DEFAULT_INDEX_PATTERNS = { fields: [], title: '' }; +const DEFAULT_DOC_VALUE_FIELDS: DocValueFields[] = []; + export const useFetchIndexPatterns = (defaultIndices: string[] = []): Return => { const apolloClient = useApolloClient(); const [indices, setIndices] = useState(defaultIndices); - const [indicesExists, setIndicesExists] = useState(false); - const [indexPatterns, setIndexPatterns] = useState({ fields: [], title: '' }); - const [browserFields, setBrowserFields] = useState({}); - const [isLoading, setIsLoading] = useState(false); + + const [state, setState] = useState({ + browserFields: DEFAULT_BROWSER_FIELDS, + docValueFields: DEFAULT_DOC_VALUE_FIELDS, + indices: defaultIndices, + indicesExists: false, + indexPatterns: DEFAULT_INDEX_PATTERNS, + isLoading: false, + }); + const [, dispatchToaster] = useStateToaster(); useEffect(() => { if (!deepEqual(defaultIndices, indices)) { setIndices(defaultIndices); + setState((prevState) => ({ ...prevState, indices: defaultIndices })); } }, [defaultIndices, indices]); @@ -52,7 +66,7 @@ export const useFetchIndexPatterns = (defaultIndices: string[] = []): Return => async function fetchIndexPatterns() { if (apolloClient && !isEmpty(indices)) { - setIsLoading(true); + setState((prevState) => ({ ...prevState, isLoading: true })); apolloClient .query({ query: sourceQuery, @@ -70,19 +84,28 @@ export const useFetchIndexPatterns = (defaultIndices: string[] = []): Return => .then( (result) => { if (isSubscribed) { - setIsLoading(false); - setIndicesExists(get('data.source.status.indicesExist', result)); - setIndexPatterns( - getIndexFields(indices.join(), get('data.source.status.indexFields', result)) - ); - setBrowserFields( - getBrowserFields(indices.join(), get('data.source.status.indexFields', result)) - ); + setState({ + browserFields: getBrowserFields( + indices.join(), + get('data.source.status.indexFields', result) + ), + docValueFields: getdocValueFields( + indices.join(), + get('data.source.status.indexFields', result) + ), + indices, + isLoading: false, + indicesExists: get('data.source.status.indicesExist', result), + indexPatterns: getIndexFields( + indices.join(), + get('data.source.status.indexFields', result) + ), + }); } }, (error) => { if (isSubscribed) { - setIsLoading(false); + setState((prevState) => ({ ...prevState, isLoading: false })); errorToToaster({ title: i18n.RULE_ADD_FAILURE, error, dispatchToaster }); } } @@ -97,5 +120,5 @@ export const useFetchIndexPatterns = (defaultIndices: string[] = []): Return => // eslint-disable-next-line react-hooks/exhaustive-deps }, [indices]); - return [{ browserFields, isLoading, indices, indicesExists, indexPatterns }, setIndices]; + return [state, setIndices]; }; 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 d5aa57ddd8754..f4004a66c8f80 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 @@ -19,9 +19,12 @@ jest.mock('../../components/user_info'); jest.mock('../../../common/containers/source'); jest.mock('../../../common/components/link_to'); jest.mock('../../../common/containers/use_global_time', () => ({ - useGlobalTime: jest - .fn() - .mockReturnValue({ from: 0, isInitializing: false, to: 0, setQuery: jest.fn() }), + 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('react-router-dom', () => { const originalModule = jest.requireActual('react-router-dom'); 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 84cfc744312f9..cdff8ea4ab928 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 @@ -70,7 +70,11 @@ export const DetectionEnginePageComponent: React.FC = ({ return; } const [min, max] = x; - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + setAbsoluteRangeDatePicker({ + id: 'global', + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }); }, [setAbsoluteRangeDatePicker] ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx index 0a42602e5fbb2..f4b112d465260 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx @@ -20,9 +20,12 @@ jest.mock('../../../../../common/components/link_to'); jest.mock('../../../../components/user_info'); jest.mock('../../../../../common/containers/source'); jest.mock('../../../../../common/containers/use_global_time', () => ({ - useGlobalTime: jest - .fn() - .mockReturnValue({ from: 0, isInitializing: false, to: 0, setQuery: jest.fn() }), + 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('react-router-dom', () => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index c74a2a3cf993a..45a1c89cec621 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -236,7 +236,11 @@ export const RuleDetailsPageComponent: FC = ({ return; } const [min, max] = x; - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + setAbsoluteRangeDatePicker({ + id: 'global', + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }); }, [setAbsoluteRangeDatePicker] ); diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 4716440c36e61..4e91324ecc9ff 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -735,6 +735,28 @@ } }, "defaultValue": null + }, + { + "name": "docValueFields", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "docValueFieldsInput", + "ofType": null + } + } + } + }, + "defaultValue": null } ], "type": { @@ -816,6 +838,28 @@ } }, "defaultValue": null + }, + { + "name": "docValueFields", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "docValueFieldsInput", + "ofType": null + } + } + } + }, + "defaultValue": null } ], "type": { @@ -867,6 +911,28 @@ } }, "defaultValue": null + }, + { + "name": "docValueFields", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "docValueFieldsInput", + "ofType": null + } + } + } + }, + "defaultValue": null } ], "type": { @@ -924,6 +990,28 @@ } }, "defaultValue": null + }, + { + "name": "docValueFields", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "docValueFieldsInput", + "ofType": null + } + } + } + }, + "defaultValue": null } ], "type": { @@ -1001,6 +1089,28 @@ } }, "defaultValue": null + }, + { + "name": "docValueFields", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "docValueFieldsInput", + "ofType": null + } + } + } + }, + "defaultValue": null } ], "type": { @@ -1105,6 +1215,28 @@ } }, "defaultValue": null + }, + { + "name": "docValueFields", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "docValueFieldsInput", + "ofType": null + } + } + } + }, + "defaultValue": null } ], "type": { @@ -1158,6 +1290,28 @@ } }, "defaultValue": null + }, + { + "name": "docValueFields", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "docValueFieldsInput", + "ofType": null + } + } + } + }, + "defaultValue": null } ], "type": { "kind": "OBJECT", "name": "IpOverviewData", "ofType": null }, @@ -1817,6 +1971,28 @@ "description": "", "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "defaultValue": null + }, + { + "name": "docValueFields", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "docValueFieldsInput", + "ofType": null + } + } + } + }, + "defaultValue": null } ], "type": { @@ -2522,7 +2698,7 @@ "type": { "kind": "NON_NULL", "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } }, "defaultValue": null }, @@ -2532,7 +2708,7 @@ "type": { "kind": "NON_NULL", "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } }, "defaultValue": null } @@ -2592,6 +2768,37 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "docValueFieldsInput", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "field", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "format", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "AuthenticationsData", @@ -10219,7 +10426,7 @@ "name": "start", "description": "", "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "type": { "kind": "SCALAR", "name": "ToAny", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, @@ -10227,7 +10434,7 @@ "name": "end", "description": "", "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "type": { "kind": "SCALAR", "name": "ToAny", "ofType": null }, "isDeprecated": false, "deprecationReason": null } @@ -11705,13 +11912,13 @@ { "name": "start", "description": "", - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "type": { "kind": "SCALAR", "name": "ToAny", "ofType": null }, "defaultValue": null }, { "name": "end", "description": "", - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "type": { "kind": "SCALAR", "name": "ToAny", "ofType": null }, "defaultValue": null } ], diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 98addf3317ff4..5f8595df23f9b 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -24,9 +24,9 @@ export interface TimerangeInput { /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */ interval: string; /** The end of the timerange */ - to: number; + to: string; /** The beginning of the timerange */ - from: number; + from: string; } export interface PaginationInputPaginated { @@ -40,6 +40,12 @@ export interface PaginationInputPaginated { querySize: number; } +export interface DocValueFieldsInput { + field: string; + + format: string; +} + export interface PaginationInput { /** The limit parameter allows you to configure the maximum amount of items to be returned */ limit: number; @@ -260,9 +266,9 @@ export interface KueryFilterQueryInput { } export interface DateRangePickerInput { - start?: Maybe; + start?: Maybe; - end?: Maybe; + end?: Maybe; } export interface SortTimelineInput { @@ -2093,9 +2099,9 @@ export interface QueryMatchResult { } export interface DateRangePickerResult { - start?: Maybe; + start?: Maybe; - end?: Maybe; + end?: Maybe; } export interface FavoriteTimelineResult { @@ -2332,6 +2338,8 @@ export interface AuthenticationsSourceArgs { filterQuery?: Maybe; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface TimelineSourceArgs { pagination: PaginationInput; @@ -2345,6 +2353,8 @@ export interface TimelineSourceArgs { filterQuery?: Maybe; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface TimelineDetailsSourceArgs { eventId: string; @@ -2352,6 +2362,8 @@ export interface TimelineDetailsSourceArgs { indexName: string; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface LastEventTimeSourceArgs { id?: Maybe; @@ -2361,6 +2373,8 @@ export interface LastEventTimeSourceArgs { details: LastTimeDetails; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface HostsSourceArgs { id?: Maybe; @@ -2374,6 +2388,8 @@ export interface HostsSourceArgs { filterQuery?: Maybe; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface HostOverviewSourceArgs { id?: Maybe; @@ -2390,6 +2406,8 @@ export interface HostFirstLastSeenSourceArgs { hostName: string; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface IpOverviewSourceArgs { id?: Maybe; @@ -2399,6 +2417,8 @@ export interface IpOverviewSourceArgs { ip: string; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface UsersSourceArgs { filterQuery?: Maybe; @@ -2514,6 +2534,8 @@ export interface NetworkDnsHistogramSourceArgs { timerange: TimerangeInput; stackByField?: Maybe; + + docValueFields: DocValueFieldsInput[]; } export interface NetworkHttpSourceArgs { id?: Maybe; @@ -2632,6 +2654,7 @@ export namespace GetLastEventTimeQuery { indexKey: LastEventIndexKey; details: LastTimeDetails; defaultIndex: string[]; + docValueFields: DocValueFieldsInput[]; }; export type Query = { @@ -2768,6 +2791,7 @@ export namespace GetAuthenticationsQuery { filterQuery?: Maybe; defaultIndex: string[]; inspect: boolean; + docValueFields: DocValueFieldsInput[]; }; export type Query = { @@ -2904,6 +2928,7 @@ export namespace GetHostFirstLastSeenQuery { sourceId: string; hostName: string; defaultIndex: string[]; + docValueFields: DocValueFieldsInput[]; }; export type Query = { @@ -2938,6 +2963,7 @@ export namespace GetHostsTableQuery { filterQuery?: Maybe; defaultIndex: string[]; inspect: boolean; + docValueFields: DocValueFieldsInput[]; }; export type Query = { @@ -3379,6 +3405,7 @@ export namespace GetIpOverviewQuery { ip: string; defaultIndex: string[]; inspect: boolean; + docValueFields: DocValueFieldsInput[]; }; export type Query = { @@ -4541,6 +4568,7 @@ export namespace GetTimelineDetailsQuery { eventId: string; indexName: string; defaultIndex: string[]; + docValueFields: DocValueFieldsInput[]; }; export type Query = { @@ -4615,6 +4643,8 @@ export namespace GetTimelineQuery { filterQuery?: Maybe; defaultIndex: string[]; inspect: boolean; + docValueFields: DocValueFieldsInput[]; + timerange: TimerangeInput; }; export type Query = { @@ -5644,9 +5674,9 @@ export namespace GetOneTimeline { export type DateRange = { __typename?: 'DateRangePickerResult'; - start: Maybe; + start: Maybe; - end: Maybe; + end: Maybe; }; export type EventIdToNoteIds = { @@ -6030,9 +6060,9 @@ export namespace PersistTimelineMutation { export type DateRange = { __typename?: 'DateRangePickerResult'; - start: Maybe; + start: Maybe; - end: Maybe; + end: Maybe; }; export type Sort = { diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.test.tsx index 09e253ae56747..978bdcaa2bb01 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.test.tsx @@ -14,8 +14,8 @@ import { kpiHostDetailsMapping } from './kpi_host_details_mapping'; describe('kpiHostsComponent', () => { const ID = 'kpiHost'; - const from = new Date('2019-06-15T06:00:00.000Z').valueOf(); - const to = new Date('2019-06-18T06:00:00.000Z').valueOf(); + const from = '2019-06-15T06:00:00.000Z'; + const to = '2019-06-18T06:00:00.000Z'; const narrowDateRange = () => {}; describe('render', () => { test('it should render spinner if it is loading', () => { diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx index ba70df7d361d4..c39e86591013f 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx @@ -21,10 +21,10 @@ import { UpdateDateRange } from '../../../common/components/charts/common'; const kpiWidgetHeight = 247; interface GenericKpiHostProps { - from: number; + from: string; id: string; loading: boolean; - to: number; + to: string; narrowDateRange: UpdateDateRange; } diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.gql_query.ts b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.gql_query.ts index eee35730cfdbb..c68816b34c175 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.gql_query.ts +++ b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.gql_query.ts @@ -14,6 +14,7 @@ export const authenticationsQuery = gql` $filterQuery: String $defaultIndex: [String!]! $inspect: Boolean! + $docValueFields: [docValueFieldsInput!]! ) { source(id: $sourceId) { id @@ -22,6 +23,7 @@ export const authenticationsQuery = gql` pagination: $pagination filterQuery: $filterQuery defaultIndex: $defaultIndex + docValueFields: $docValueFields ) { totalCount edges { diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx index bfada0583f8e9..efd80c5c590ed 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx @@ -63,6 +63,7 @@ class AuthenticationsComponentQuery extends QueryTemplatePaginated< const { activePage, children, + docValueFields, endDate, filterQuery, id = ID, @@ -84,6 +85,7 @@ class AuthenticationsComponentQuery extends QueryTemplatePaginated< filterQuery: createFilter(filterQuery), defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), inspect: isInspected, + docValueFields: docValueFields ?? [], }; return ( diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/first_last_seen.gql_query.ts b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/first_last_seen.gql_query.ts index 7db4f138c7794..18cbcf516839f 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/first_last_seen.gql_query.ts +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/first_last_seen.gql_query.ts @@ -7,10 +7,19 @@ import gql from 'graphql-tag'; export const HostFirstLastSeenGqlQuery = gql` - query GetHostFirstLastSeenQuery($sourceId: ID!, $hostName: String!, $defaultIndex: [String!]!) { + query GetHostFirstLastSeenQuery( + $sourceId: ID! + $hostName: String! + $defaultIndex: [String!]! + $docValueFields: [docValueFieldsInput!]! + ) { source(id: $sourceId) { id - HostFirstLastSeen(hostName: $hostName, defaultIndex: $defaultIndex) { + HostFirstLastSeen( + hostName: $hostName + defaultIndex: $defaultIndex + docValueFields: $docValueFields + ) { firstSeen lastSeen } diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.ts b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.ts index a4f8fca23e8aa..65e379b5ba2d8 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.ts +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.ts @@ -13,7 +13,7 @@ import { useUiSetting$ } from '../../../../common/lib/kibana'; import { GetHostFirstLastSeenQuery } from '../../../../graphql/types'; import { inputsModel } from '../../../../common/store'; import { QueryTemplateProps } from '../../../../common/containers/query_template'; - +import { useWithSource } from '../../../../common/containers/source'; import { HostFirstLastSeenGqlQuery } from './first_last_seen.gql_query'; export interface FirstLastSeenHostArgs { @@ -40,6 +40,7 @@ export function useFirstLastSeenHostQuery( const [lastSeen, updateLastSeen] = useState(null); const [errorMessage, updateErrorMessage] = useState(null); const [defaultIndex] = useUiSetting$(DEFAULT_INDEX_KEY); + const { docValueFields } = useWithSource(sourceId); async function fetchFirstLastSeenHost(signal: AbortSignal) { updateLoading(true); @@ -51,6 +52,7 @@ export function useFirstLastSeenHostQuery( sourceId, hostName, defaultIndex, + docValueFields, }, context: { fetchOptions: { diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/mock.ts b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/mock.ts index 51e484ffbd859..7f1b3d97eb525 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/mock.ts +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/mock.ts @@ -35,6 +35,7 @@ export const mockFirstLastSeenHostQuery: MockedProvidedQuery[] = [ sourceId: 'default', hostName: 'kibana-siem', defaultIndex: DEFAULT_INDEX_PATTERN, + docValueFields: [], }, }, result: { diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/hosts_table.gql_query.ts b/x-pack/plugins/security_solution/public/hosts/containers/hosts/hosts_table.gql_query.ts index 672ea70b09ad2..e93f3e379b30e 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/hosts_table.gql_query.ts +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/hosts_table.gql_query.ts @@ -15,6 +15,7 @@ export const HostsTableQuery = gql` $filterQuery: String $defaultIndex: [String!]! $inspect: Boolean! + $docValueFields: [docValueFieldsInput!]! ) { source(id: $sourceId) { id @@ -24,6 +25,7 @@ export const HostsTableQuery = gql` sort: $sort filterQuery: $filterQuery defaultIndex: $defaultIndex + docValueFields: $docValueFields ) { totalCount edges { diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx index 70f21b6f23cc0..8af24e6e6abc1 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx @@ -33,7 +33,7 @@ import { generateTablePaginationOptions } from '../../../common/components/pagin const ID = 'hostsQuery'; export interface HostsArgs { - endDate: number; + endDate: string; hosts: HostsEdges[]; id: string; inspect: inputsModel.InspectQuery; @@ -42,15 +42,15 @@ export interface HostsArgs { loadPage: (newActivePage: number) => void; pageInfo: PageInfoPaginated; refetch: inputsModel.Refetch; - startDate: number; + startDate: string; totalCount: number; } export interface OwnProps extends QueryTemplatePaginatedProps { children: (args: HostsArgs) => React.ReactNode; type: hostsModel.HostsType; - startDate: number; - endDate: number; + startDate: string; + endDate: string; } export interface HostsComponentReduxProps { @@ -81,6 +81,7 @@ class HostsComponentQuery extends QueryTemplatePaginated< public render() { const { activePage, + docValueFields, id = ID, isInspected, children, @@ -110,6 +111,7 @@ class HostsComponentQuery extends QueryTemplatePaginated< pagination: generateTablePaginationOptions(activePage, limit), filterQuery: createFilter(filterQuery), defaultIndex, + docValueFields: docValueFields ?? [], inspect: isInspected, }; return ( diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/index.tsx index 5267fff3a26d6..12a82c7980b61 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/index.tsx @@ -27,8 +27,8 @@ export interface HostOverviewArgs { hostOverview: HostItem; loading: boolean; refetch: inputsModel.Refetch; - startDate: number; - endDate: number; + startDate: string; + endDate: string; } export interface HostOverviewReduxProps { @@ -38,8 +38,8 @@ export interface HostOverviewReduxProps { export interface OwnProps extends QueryTemplateProps { children: (args: HostOverviewArgs) => React.ReactNode; hostName: string; - startDate: number; - endDate: number; + startDate: string; + endDate: string; } type HostsOverViewProps = OwnProps & HostOverviewReduxProps & WithKibanaProps; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx index cce48a1e605b2..08fe48c0dd709 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx @@ -17,14 +17,19 @@ import { type } from './utils'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { getHostDetailsPageFilters } from './helpers'; +jest.mock('../../../common/components/url_state/normalize_time_range.ts'); + jest.mock('../../../common/containers/source', () => ({ useWithSource: jest.fn().mockReturnValue({ indicesExist: true, indexPattern: mockIndexPattern }), })); jest.mock('../../../common/containers/use_global_time', () => ({ - useGlobalTime: jest - .fn() - .mockReturnValue({ from: 0, isInitializing: false, to: 0, setQuery: jest.fn() }), + useGlobalTime: jest.fn().mockReturnValue({ + from: '2020-07-07T08:20:18.966Z', + isInitializing: false, + to: '2020-07-08T08:20:18.966Z', + setQuery: jest.fn(), + }), })); // Test will fail because we will to need to mock some core services to make the test work @@ -73,17 +78,17 @@ describe('body', () => { @@ -91,10 +96,10 @@ describe('body', () => { // match against everything but the functions to ensure they are there as expected expect(wrapper.find(componentName).props()).toMatchObject({ - endDate: 0, + endDate: '2020-07-08T08:20:18.966Z', filterQuery, skip: false, - startDate: 0, + startDate: '2020-07-07T08:20:18.966Z', type: 'details', indexPattern: { fields: [ diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx index acde0cbe1d42b..4d4eead0e778a 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx @@ -28,6 +28,7 @@ import { export const HostDetailsTabs = React.memo( ({ + docValueFields, pageFilters, filterQuery, detailName, @@ -54,7 +55,11 @@ export const HostDetailsTabs = React.memo( return; } const [min, max] = x; - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + setAbsoluteRangeDatePicker({ + id: 'global', + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }); }, [setAbsoluteRangeDatePicker] ); @@ -76,7 +81,7 @@ export const HostDetailsTabs = React.memo( return ( - + diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index bb0317f0482b0..447d003625c8f 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -73,11 +73,15 @@ const HostDetailsComponent = React.memo( return; } const [min, max] = x; - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + setAbsoluteRangeDatePicker({ + id: 'global', + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }); }, [setAbsoluteRangeDatePicker] ); - const { indicesExist, indexPattern } = useWithSource(); + const { docValueFields, indicesExist, indexPattern } = useWithSource(); const filterQuery = convertToBuildEsQuery({ config: esQuery.getEsQueryConfig(kibana.services.uiSettings), indexPattern, @@ -175,6 +179,7 @@ const HostDetailsComponent = React.memo( ; detailName: string; hostDetailsPagePath: string; @@ -56,6 +57,7 @@ export type HostDetailsNavTab = Record; export type HostDetailsTabsProps = HostBodyComponentDispatchProps & HostsQueryProps & { + docValueFields?: DocValueFields[]; pageFilters?: Filter[]; filterQuery: string; indexPattern: IIndexPattern; @@ -64,6 +66,6 @@ export type HostDetailsTabsProps = HostBodyComponentDispatchProps & export type SetAbsoluteRangeDatePicker = ActionCreator<{ id: InputsModelId; - from: number; - to: number; + from: string; + to: string; }>; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index a2f83bf0965f3..b37d91cc2be3b 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -62,11 +62,15 @@ export const HostsComponent = React.memo( return; } const [min, max] = x; - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + setAbsoluteRangeDatePicker({ + id: 'global', + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }); }, [setAbsoluteRangeDatePicker] ); - const { indicesExist, indexPattern } = useWithSource(); + const { docValueFields, indicesExist, indexPattern } = useWithSource(); const filterQuery = convertToBuildEsQuery({ config: esQuery.getEsQueryConfig(kibana.services.uiSettings), indexPattern, @@ -125,6 +129,7 @@ export const HostsComponent = React.memo( ( ({ deleteQuery, + docValueFields, filterQuery, setAbsoluteRangeDatePicker, to, @@ -62,7 +63,11 @@ export const HostsTabs = memo( return; } const [min, max] = x; - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + setAbsoluteRangeDatePicker({ + id: 'global', + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }); }, [setAbsoluteRangeDatePicker] ), @@ -71,10 +76,10 @@ export const HostsTabs = memo( return ( - + - + diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx index 41f5b7816205e..88886a874a949 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx @@ -61,6 +61,7 @@ const histogramConfigs: MatrixHisrogramConfigs = { export const AuthenticationsQueryTabBody = ({ deleteQuery, + docValueFields, endDate, filterQuery, skip, @@ -89,6 +90,7 @@ export const AuthenticationsQueryTabBody = ({ {...histogramConfigs} /> ( ; }; diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx index 76e197063fb8a..d7e9d86916c6d 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx @@ -26,11 +26,11 @@ describe('EmbeddedMapComponent', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( ); expect(wrapper).toMatchSnapshot(); diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx index 81aa4b1671fca..828e4d3eaaaa0 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx @@ -71,8 +71,8 @@ EmbeddableMap.displayName = 'EmbeddableMap'; export interface EmbeddedMapProps { query: Query; filters: Filter[]; - startDate: number; - endDate: number; + startDate: string; + endDate: string; setQuery: GlobalTimeArgs['setQuery']; } diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.test.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.test.tsx index 50170f4f6ae9e..0c6b90ec2b9dd 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.test.tsx @@ -35,8 +35,8 @@ describe('embedded_map_helpers', () => { [], [], { query: '', language: 'kuery' }, - 0, - 0, + '2020-07-07T08:20:18.966Z', + '2020-07-08T08:20:18.966Z', setQueryMock, createPortalNode(), mockEmbeddable @@ -50,8 +50,8 @@ describe('embedded_map_helpers', () => { [], [], { query: '', language: 'kuery' }, - 0, - 0, + '2020-07-07T08:20:18.966Z', + '2020-07-08T08:20:18.966Z', setQueryMock, createPortalNode(), mockEmbeddable diff --git a/x-pack/plugins/security_solution/public/network/components/ip_overview/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/ip_overview/__snapshots__/index.test.tsx.snap index fe34c584bafb7..ca2ce4ee921c7 100644 --- a/x-pack/plugins/security_solution/public/network/components/ip_overview/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/ip_overview/__snapshots__/index.test.tsx.snap @@ -137,14 +137,14 @@ exports[`IP Overview Component rendering it renders the default IP Overview 1`] "interval": "day", } } - endDate={1560837600000} + endDate="2019-06-18T06:00:00.000Z" flowTarget="source" id="ipOverview" ip="10.10.10.10" isLoadingAnomaliesData={false} loading={false} narrowDateRange={[MockFunction]} - startDate={1560578400000} + startDate="2019-06-15T06:00:00.000Z" type="details" updateFlowTargetAction={[MockFunction]} /> diff --git a/x-pack/plugins/security_solution/public/network/components/ip_overview/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/ip_overview/index.test.tsx index b8d97f06bf85f..b9d9279ae34f8 100644 --- a/x-pack/plugins/security_solution/public/network/components/ip_overview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/ip_overview/index.test.tsx @@ -51,14 +51,14 @@ describe('IP Overview Component', () => { const mockProps = { anomaliesData: mockAnomalies, data: mockData.IpOverview, - endDate: new Date('2019-06-18T06:00:00.000Z').valueOf(), + endDate: '2019-06-18T06:00:00.000Z', flowTarget: FlowTarget.source, loading: false, id: 'ipOverview', ip: '10.10.10.10', isLoadingAnomaliesData: false, narrowDateRange: (jest.fn() as unknown) as NarrowDateRange, - startDate: new Date('2019-06-15T06:00:00.000Z').valueOf(), + startDate: '2019-06-15T06:00:00.000Z', type: networkModel.NetworkType.details, updateFlowTargetAction: (jest.fn() as unknown) as ActionCreator<{ flowTarget: FlowTarget; diff --git a/x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx b/x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx index 56f6d27dc28ca..cf08b084d2197 100644 --- a/x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx @@ -42,8 +42,8 @@ interface OwnProps { loading: boolean; isLoadingAnomaliesData: boolean; anomaliesData: Anomalies | null; - startDate: number; - endDate: number; + startDate: string; + endDate: string; type: networkModel.NetworkType; narrowDateRange: NarrowDateRange; } diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/kpi_network/__snapshots__/index.test.tsx.snap index ee7649b00aed1..2f97e45b217f3 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/__snapshots__/index.test.tsx.snap @@ -32,11 +32,11 @@ exports[`KpiNetwork Component rendering it renders loading icons 1`] = ` ], } } - from={1560578400000} + from="2019-06-15T06:00:00.000Z" id="kpiNetwork" loading={true} narrowDateRange={[MockFunction]} - to={1560837600000} + to="2019-06-18T06:00:00.000Z" /> `; @@ -72,10 +72,10 @@ exports[`KpiNetwork Component rendering it renders the default widget 1`] = ` ], } } - from={1560578400000} + from="2019-06-15T06:00:00.000Z" id="kpiNetwork" loading={false} narrowDateRange={[MockFunction]} - to={1560837600000} + to="2019-06-18T06:00:00.000Z" /> `; diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/index.test.tsx index 8acd17d2ce767..06f623e61c280 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/index.test.tsx @@ -21,8 +21,8 @@ import { mockData } from './mock'; describe('KpiNetwork Component', () => { const state: State = mockGlobalState; - const from = new Date('2019-06-15T06:00:00.000Z').valueOf(); - const to = new Date('2019-06-18T06:00:00.000Z').valueOf(); + const from = '2019-06-15T06:00:00.000Z'; + const to = '2019-06-18T06:00:00.000Z'; const narrowDateRange = jest.fn(); const { storage } = createSecuritySolutionStorageMock(); diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/index.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/index.tsx index ac7381160515d..dd8979bc02a61 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/index.tsx @@ -37,10 +37,10 @@ const euiColorVis3 = euiVisColorPalette[3]; interface KpiNetworkProps { data: KpiNetworkData; - from: number; + from: string; id: string; loading: boolean; - to: number; + to: string; narrowDateRange: UpdateDateRange; } @@ -132,8 +132,8 @@ export const KpiNetworkBaseComponent = React.memo<{ fieldsMapping: Readonly; data: KpiNetworkData; id: string; - from: number; - to: number; + from: string; + to: string; narrowDateRange: UpdateDateRange; }>(({ fieldsMapping, data, id, from, to, narrowDateRange }) => { const statItemsProps: StatItemsProps[] = useKpiMatrixStatus( diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/mock.ts b/x-pack/plugins/security_solution/public/network/components/kpi_network/mock.ts index a8b04ff29f4b6..bd820d4ed367d 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/mock.ts +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/mock.ts @@ -220,11 +220,11 @@ export const mockEnableChartsData = { icon: 'visMapCoordinate', }, ], - from: 1560578400000, + from: '2019-06-15T06:00:00.000Z', grow: 2, id: 'statItem', index: 2, statKey: 'UniqueIps', - to: 1560837600000, + to: '2019-06-18T06:00:00.000Z', narrowDateRange: mockNarrowDateRange, }; diff --git a/x-pack/plugins/security_solution/public/network/containers/ip_overview/index.gql_query.ts b/x-pack/plugins/security_solution/public/network/containers/ip_overview/index.gql_query.ts index 3733cd780a4f7..6ebb60ccb4ea6 100644 --- a/x-pack/plugins/security_solution/public/network/containers/ip_overview/index.gql_query.ts +++ b/x-pack/plugins/security_solution/public/network/containers/ip_overview/index.gql_query.ts @@ -13,10 +13,16 @@ export const ipOverviewQuery = gql` $ip: String! $defaultIndex: [String!]! $inspect: Boolean! + $docValueFields: [docValueFieldsInput!]! ) { source(id: $sourceId) { id - IpOverview(filterQuery: $filterQuery, ip: $ip, defaultIndex: $defaultIndex) { + IpOverview( + filterQuery: $filterQuery + ip: $ip + defaultIndex: $defaultIndex + docValueFields: $docValueFields + ) { source { firstSeen lastSeen diff --git a/x-pack/plugins/security_solution/public/network/containers/ip_overview/index.tsx b/x-pack/plugins/security_solution/public/network/containers/ip_overview/index.tsx index 551ecebf2c05a..6c8b54cc79517 100644 --- a/x-pack/plugins/security_solution/public/network/containers/ip_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/ip_overview/index.tsx @@ -35,7 +35,7 @@ export interface IpOverviewProps extends QueryTemplateProps { } const IpOverviewComponentQuery = React.memo( - ({ id = ID, isInspected, children, filterQuery, skip, sourceId, ip }) => ( + ({ id = ID, docValueFields, isInspected, children, filterQuery, skip, sourceId, ip }) => ( query={ipOverviewQuery} fetchPolicy={getDefaultFetchPolicy()} @@ -46,6 +46,7 @@ const IpOverviewComponentQuery = React.memo( filterQuery: createFilter(filterQuery), ip, defaultIndex: useUiSetting(DEFAULT_INDEX_KEY), + docValueFields: docValueFields ?? [], inspect: isInspected, }} > diff --git a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx index a50f2a131b75b..17506f9a01cb9 100644 --- a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx @@ -92,8 +92,8 @@ class TlsComponentQuery extends QueryTemplatePaginated< sourceId, timerange: { interval: '12h', - from: startDate ? startDate : 0, - to: endDate ? endDate : Date.now(), + from: startDate ? startDate : '', + to: endDate ? endDate : new Date(Date.now()).toISOString(), }, }; return ( diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx index 92f39228f07a7..e2e458bcec2f5 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx @@ -34,9 +34,12 @@ type GlobalWithFetch = NodeJS.Global & { fetch: jest.Mock }; jest.mock('../../../common/lib/kibana'); jest.mock('../../../common/containers/source'); jest.mock('../../../common/containers/use_global_time', () => ({ - useGlobalTime: jest - .fn() - .mockReturnValue({ from: 0, isInitializing: false, to: 0, setQuery: jest.fn() }), + useGlobalTime: jest.fn().mockReturnValue({ + from: '2020-07-07T08:20:18.966Z', + isInitializing: false, + to: '2020-07-08T08:20:18.966Z', + setQuery: jest.fn(), + }), })); // Test will fail because we will to need to mock some core services to make the test work @@ -67,8 +70,8 @@ const getMockHistory = (ip: string) => ({ listen: jest.fn(), }); -const to = new Date('2018-03-23T18:49:23.132Z').valueOf(); -const from = new Date('2018-03-24T03:33:52.253Z').valueOf(); +const to = '2018-03-23T18:49:23.132Z'; +const from = '2018-03-24T03:33:52.253Z'; const getMockProps = (ip: string) => ({ to, from, @@ -88,8 +91,8 @@ const getMockProps = (ip: string) => ({ match: { params: { detailName: ip, search: '' }, isExact: true, path: '', url: '' }, setAbsoluteRangeDatePicker: (jest.fn() as unknown) as ActionCreator<{ id: InputsModelId; - from: number; - to: number; + from: string; + to: string; }>, setIpDetailsTablesActivePageToZero: (jest.fn() as unknown) as ActionCreator, }); diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx index 5eb7a1cec6760..e06f5489a3fc2 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx @@ -77,7 +77,7 @@ export const IPDetailsComponent: React.FC ( return; } const [min, max] = x; - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + setAbsoluteRangeDatePicker({ + id: 'global', + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }); }, [setAbsoluteRangeDatePicker] ); diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts b/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts index 6986d10ad3523..183c760e40ab1 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts @@ -18,8 +18,8 @@ import { NarrowDateRange } from '../../../common/components/ml/types'; interface QueryTabBodyProps extends Pick { skip: boolean; type: networkModel.NetworkType; - startDate: number; - endDate: number; + startDate: string; + endDate: string; filterQuery?: string | ESTermQuery; narrowDateRange?: NarrowDateRange; } diff --git a/x-pack/plugins/security_solution/public/network/pages/network.test.tsx b/x-pack/plugins/security_solution/public/network/pages/network.test.tsx index af84e1d42b45b..78521a980de40 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.test.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.test.tsx @@ -58,8 +58,8 @@ const mockHistory = { listen: jest.fn(), }; -const to = new Date('2018-03-23T18:49:23.132Z').valueOf(); -const from = new Date('2018-03-24T03:33:52.253Z').valueOf(); +const to = '2018-03-23T18:49:23.132Z'; +const from = '2018-03-24T03:33:52.253Z'; const getMockProps = () => ({ networkPagePath: '', diff --git a/x-pack/plugins/security_solution/public/network/pages/network.tsx b/x-pack/plugins/security_solution/public/network/pages/network.tsx index 5767951f9f6b3..f8927096c1a61 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.tsx @@ -68,7 +68,11 @@ const NetworkComponent = React.memo( return; } const [min, max] = x; - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + setAbsoluteRangeDatePicker({ + id: 'global', + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }); }, [setAbsoluteRangeDatePicker] ); diff --git a/x-pack/plugins/security_solution/public/network/pages/types.ts b/x-pack/plugins/security_solution/public/network/pages/types.ts index 54ff5a8d50b8e..db3546409c8d9 100644 --- a/x-pack/plugins/security_solution/public/network/pages/types.ts +++ b/x-pack/plugins/security_solution/public/network/pages/types.ts @@ -10,8 +10,8 @@ import { InputsModelId } from '../../common/store/inputs/constants'; export type SetAbsoluteRangeDatePicker = ActionCreator<{ id: InputsModelId; - from: number; - to: number; + from: string; + to: string; }>; export type NetworkComponentProps = Partial> & { diff --git a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx index d2d9861e0ae1a..8d004829a34f0 100644 --- a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx @@ -26,8 +26,8 @@ jest.mock('../../../common/containers/matrix_histogram', () => { }); const theme = () => ({ eui: { ...euiDarkVars, euiSizeL: '24px' }, darkMode: true }); -const from = new Date('2020-03-31T06:00:00.000Z').valueOf(); -const to = new Date('2019-03-31T06:00:00.000Z').valueOf(); +const from = '2020-03-31T06:00:00.000Z'; +const to = '2019-03-31T06:00:00.000Z'; describe('Alerts by category', () => { let wrapper: ReactWrapper; diff --git a/x-pack/plugins/security_solution/public/overview/components/event_counts/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/event_counts/index.test.tsx index 95dd65f559470..c4a941d845f16 100644 --- a/x-pack/plugins/security_solution/public/overview/components/event_counts/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/event_counts/index.test.tsx @@ -16,8 +16,8 @@ import { EventCounts } from '.'; jest.mock('../../../common/components/link_to'); describe('EventCounts', () => { - const from = 1579553397080; - const to = 1579639797080; + const from = '2020-01-20T20:49:57.080Z'; + const to = '2020-01-21T20:49:57.080Z'; test('it filters the `Host events` widget with a `host.name` `exists` filter', () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/overview/components/host_overview/__snapshots__/index.test.tsx.snap index e5a4df59ac7e4..c9c34682519e2 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/__snapshots__/index.test.tsx.snap @@ -192,11 +192,11 @@ exports[`Host Summary Component rendering it renders the default Host Summary 1` }, } } - endDate={1560837600000} + endDate="2019-06-18T06:00:00.000Z" id="hostOverview" isLoadingAnomaliesData={false} loading={false} narrowDateRange={[MockFunction]} - startDate={1560578400000} + startDate="2019-06-15T06:00:00.000Z" /> `; diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx index 0286961fd78af..71cf056f3eb62 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx @@ -19,12 +19,12 @@ describe('Host Summary Component', () => { ); diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx index 0c679cc94f787..0a15b039b96af 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx @@ -41,8 +41,8 @@ interface HostSummaryProps { loading: boolean; isLoadingAnomaliesData: boolean; anomaliesData: Anomalies | null; - startDate: number; - endDate: number; + startDate: string; + endDate: string; narrowDateRange: NarrowDateRange; } diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx index d019a480a8045..5140137ce1b99 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx @@ -28,8 +28,8 @@ import { wait } from '../../../common/lib/helpers'; jest.mock('../../../common/lib/kibana'); jest.mock('../../../common/components/link_to'); -const startDate = 1579553397080; -const endDate = 1579639797080; +const startDate = '2020-01-20T20:49:57.080Z'; +const endDate = '2020-01-21T20:49:57.080Z'; interface MockedProvidedQuery { request: { diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx index c7f7c4f4af254..d2d823f625690 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx @@ -43,8 +43,8 @@ jest.mock('../../../common/lib/kibana', () => { }; }); -const startDate = 1579553397080; -const endDate = 1579639797080; +const startDate = '2020-01-20T20:49:57.080Z'; +const endDate = '2020-01-21T20:49:57.080Z'; interface MockedProvidedQuery { request: { diff --git a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx index 2fddb996ccef3..fbfdefa13d738 100644 --- a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx @@ -52,7 +52,11 @@ const SignalsByCategoryComponent: React.FC = ({ return; } const [min, max] = x; - setAbsoluteRangeDatePicker({ id: setAbsoluteRangeDatePickerTarget, from: min, to: max }); + setAbsoluteRangeDatePicker({ + id: setAbsoluteRangeDatePickerTarget, + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }); }, // eslint-disable-next-line react-hooks/exhaustive-deps [setAbsoluteRangeDatePicker] diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx index 89761e104d70f..76ea1f3b4af75 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx @@ -32,8 +32,8 @@ export interface OverviewHostArgs { export interface OverviewHostProps extends QueryTemplateProps { children: (args: OverviewHostArgs) => React.ReactNode; sourceId: string; - endDate: number; - startDate: number; + endDate: string; + startDate: string; } const OverviewHostComponentQuery = React.memo( diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx index 86242adf3f47f..38c035f6883b6 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx @@ -32,8 +32,8 @@ export interface OverviewNetworkArgs { export interface OverviewNetworkProps extends QueryTemplateProps { children: (args: OverviewNetworkArgs) => React.ReactNode; sourceId: string; - endDate: number; - startDate: number; + endDate: string; + startDate: string; } export const OverviewNetworkComponentQuery = React.memo( diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx index 4262afd67ba03..f7c77bc2dfdf8 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx @@ -22,9 +22,12 @@ import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enable jest.mock('../../common/lib/kibana'); jest.mock('../../common/containers/source'); jest.mock('../../common/containers/use_global_time', () => ({ - useGlobalTime: jest - .fn() - .mockReturnValue({ from: 0, isInitializing: false, to: 0, setQuery: jest.fn() }), + useGlobalTime: jest.fn().mockReturnValue({ + from: '2020-07-07T08:20:18.966Z', + isInitializing: false, + to: '2020-07-08T08:20:18.966Z', + setQuery: jest.fn(), + }), })); // Test will fail because we will to need to mock some core services to make the test work diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/mocks.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/mocks.ts index 34d763839003c..89a6dbd496bc3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/mocks.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/mocks.ts @@ -79,7 +79,7 @@ export const mockSelectedTimeline = [ }, }, title: 'duplicate timeline', - dateRange: { start: 1582538951145, end: 1582625351145 }, + dateRange: { start: '2020-02-24T10:09:11.145Z', end: '2020-02-25T10:09:11.145Z' }, savedQueryId: null, sort: { columnId: '@timestamp', sortDirection: 'desc' }, created: 1583866966262, diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index 89a35fb838a96..5759d96b95f9e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -39,6 +39,7 @@ import sinon from 'sinon'; import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; jest.mock('../../../common/store/inputs/actions'); +jest.mock('../../../common/components/url_state/normalize_time_range.ts'); jest.mock('../../store/timeline/actions'); jest.mock('../../../common/store/app/actions'); jest.mock('uuid', () => { @@ -262,10 +263,7 @@ describe('helpers', () => { }, ], dataProviders: [], - dateRange: { - end: 0, - start: 0, - }, + dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' }, description: '', deletedEventIds: [], eventIdToNoteIds: {}, @@ -360,10 +358,7 @@ describe('helpers', () => { }, ], dataProviders: [], - dateRange: { - end: 0, - start: 0, - }, + dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' }, description: '', deletedEventIds: [], eventIdToNoteIds: {}, @@ -498,6 +493,7 @@ describe('helpers', () => { ], version: '1', dataProviders: [], + dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' }, description: '', deletedEventIds: [], eventIdToNoteIds: {}, @@ -526,10 +522,6 @@ describe('helpers', () => { noteIds: [], pinnedEventIds: {}, pinnedEventsSaveObject: {}, - dateRange: { - start: 0, - end: 0, - }, selectedEventIds: {}, show: false, showCheckboxes: false, @@ -623,6 +615,7 @@ describe('helpers', () => { }, ], version: '1', + dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' }, dataProviders: [], description: '', deletedEventIds: [], @@ -695,10 +688,6 @@ describe('helpers', () => { noteIds: [], pinnedEventIds: {}, pinnedEventsSaveObject: {}, - dateRange: { - start: 0, - end: 0, - }, selectedEventIds: {}, show: false, showCheckboxes: false, @@ -757,15 +746,15 @@ describe('helpers', () => { timelineDispatch({ duplicate: true, id: 'timeline-1', - from: 1585233356356, - to: 1585233716356, + from: '2020-03-26T14:35:56.356Z', + to: '2020-03-26T14:41:56.356Z', notes: [], timeline: mockTimelineModel, })(); expect(dispatchSetTimelineRangeDatePicker).toHaveBeenCalledWith({ - from: 1585233356356, - to: 1585233716356, + from: '2020-03-26T14:35:56.356Z', + to: '2020-03-26T14:41:56.356Z', }); }); @@ -773,8 +762,8 @@ describe('helpers', () => { timelineDispatch({ duplicate: true, id: 'timeline-1', - from: 1585233356356, - to: 1585233716356, + from: '2020-03-26T14:35:56.356Z', + to: '2020-03-26T14:41:56.356Z', notes: [], timeline: mockTimelineModel, })(); @@ -789,8 +778,8 @@ describe('helpers', () => { timelineDispatch({ duplicate: true, id: 'timeline-1', - from: 1585233356356, - to: 1585233716356, + from: '2020-03-26T14:35:56.356Z', + to: '2020-03-26T14:41:56.356Z', notes: [], timeline: mockTimelineModel, })(); @@ -803,8 +792,8 @@ describe('helpers', () => { timelineDispatch({ duplicate: true, id: 'timeline-1', - from: 1585233356356, - to: 1585233716356, + from: '2020-03-26T14:35:56.356Z', + to: '2020-03-26T14:41:56.356Z', notes: [], timeline: mockTimelineModel, })(); @@ -826,8 +815,8 @@ describe('helpers', () => { timelineDispatch({ duplicate: true, id: 'timeline-1', - from: 1585233356356, - to: 1585233716356, + from: '2020-03-26T14:35:56.356Z', + to: '2020-03-26T14:41:56.356Z', notes: [], timeline: mockTimeline, })(); @@ -850,8 +839,8 @@ describe('helpers', () => { timelineDispatch({ duplicate: true, id: 'timeline-1', - from: 1585233356356, - to: 1585233716356, + from: '2020-03-26T14:35:56.356Z', + to: '2020-03-26T14:41:56.356Z', notes: [], timeline: mockTimeline, })(); @@ -879,8 +868,8 @@ describe('helpers', () => { timelineDispatch({ duplicate: false, id: 'timeline-1', - from: 1585233356356, - to: 1585233716356, + from: '2020-03-26T14:35:56.356Z', + to: '2020-03-26T14:41:56.356Z', notes: [ { created: 1585233356356, @@ -913,8 +902,8 @@ describe('helpers', () => { timelineDispatch({ duplicate: true, id: 'timeline-1', - from: 1585233356356, - to: 1585233716356, + from: '2020-03-26T14:35:56.356Z', + to: '2020-03-26T14:41:56.356Z', notes: [], timeline: mockTimelineModel, ruleNote: '# this would be some markdown', diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index 03a6d475b3426..04aef6f07c60a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -49,9 +49,9 @@ import { } from '../timeline/body/constants'; import { OpenTimelineResult, UpdateTimeline, DispatchUpdateTimeline } from './types'; -import { getTimeRangeSettings } from '../../../common/utils/default_date_settings'; import { createNote } from '../notes/helpers'; import { IS_OPERATOR } from '../timeline/data_providers/data_provider'; +import { normalizeTimeRange } from '../../../common/components/url_state/normalize_time_range'; export const OPEN_TIMELINE_CLASS_NAME = 'open-timeline'; @@ -313,10 +313,13 @@ export const queryTimelineById = ({ if (onOpenTimeline != null) { onOpenTimeline(timeline); } else if (updateTimeline) { - const { from, to } = getTimeRangeSettings(); + const { from, to } = normalizeTimeRange({ + from: getOr(null, 'dateRange.start', timeline), + to: getOr(null, 'dateRange.end', timeline), + }); updateTimeline({ duplicate, - from: getOr(from, 'dateRange.start', timeline), + from, id: 'timeline-1', notes, timeline: { @@ -324,7 +327,7 @@ export const queryTimelineById = ({ graphEventId, show: openTimeline, }, - to: getOr(to, 'dateRange.end', timeline), + to, })(); } }) diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts index a8485328e8393..eb5a03baad88c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts @@ -189,10 +189,10 @@ export interface OpenTimelineProps { export interface UpdateTimeline { duplicate: boolean; id: string; - from: number; + from: string; notes: NoteResult[] | null | undefined; timeline: TimelineModel; - to: number; + to: string; ruleNote?: string; } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap index 3508e12cb1be1..d76ddace40a5a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap @@ -804,7 +804,8 @@ In other use cases the message field can be used to concatenate different values }, ] } - end={1521862432253} + docValueFields={Array []} + end="2018-03-24T03:33:52.253Z" eventType="raw" filters={Array []} id="foo" @@ -901,6 +902,7 @@ In other use cases the message field can be used to concatenate different values } indexToAdd={Array []} isLive={false} + isLoadingSource={false} isSaving={false} itemsPerPage={5} itemsPerPageOptions={ @@ -928,7 +930,7 @@ In other use cases the message field can be used to concatenate different values "sortDirection": "desc", } } - start={1521830963132} + start="2018-03-23T18:49:23.132Z" status="active" timelineType="default" toggleColumn={[MockFunction]} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx index fc892f5b8e6b1..9f0c4747db057 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { BrowserFields } from '../../../../../common/containers/source'; +import { BrowserFields, DocValueFields } from '../../../../../common/containers/source'; import { TimelineItem, TimelineNonEcsData } from '../../../../../graphql/types'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { maxDelay } from '../../../../../common/lib/helpers/scheduler'; @@ -33,6 +33,7 @@ interface Props { columnRenderers: ColumnRenderer[]; containerElementRef: HTMLDivElement; data: TimelineItem[]; + docValueFields: DocValueFields[]; eventIdToNoteIds: Readonly>; getNotesByIds: (noteIds: string[]) => Note[]; id: string; @@ -59,6 +60,7 @@ const EventsComponent: React.FC = ({ columnRenderers, containerElementRef, data, + docValueFields, eventIdToNoteIds, getNotesByIds, id, @@ -85,6 +87,7 @@ const EventsComponent: React.FC = ({ browserFields={browserFields} columnHeaders={columnHeaders} columnRenderers={columnRenderers} + docValueFields={docValueFields} event={event} eventIdToNoteIds={eventIdToNoteIds} getNotesByIds={getNotesByIds} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index d2175c728aa2a..f93a152211a66 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -9,7 +9,7 @@ import { useSelector } from 'react-redux'; import uuid from 'uuid'; import VisibilitySensor from 'react-visibility-sensor'; -import { BrowserFields } from '../../../../../common/containers/source'; +import { BrowserFields, DocValueFields } from '../../../../../common/containers/source'; import { TimelineDetailsQuery } from '../../../../containers/details'; import { TimelineItem, DetailItem, TimelineNonEcsData } from '../../../../../graphql/types'; import { requestIdleCallbackViaScheduler } from '../../../../../common/lib/helpers/scheduler'; @@ -43,6 +43,7 @@ interface Props { browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; columnRenderers: ColumnRenderer[]; + docValueFields: DocValueFields[]; event: TimelineItem; eventIdToNoteIds: Readonly>; getNotesByIds: (noteIds: string[]) => Note[]; @@ -108,6 +109,7 @@ const StatefulEventComponent: React.FC = ({ containerElementRef, columnHeaders, columnRenderers, + docValueFields, event, eventIdToNoteIds, getNotesByIds, @@ -202,6 +204,7 @@ const StatefulEventComponent: React.FC = ({ if (isVisible) { return ( { columnHeaders: defaultHeaders, columnRenderers, data: mockTimelineData, + docValueFields: [], eventIdToNoteIds: {}, height: testBodyHeight, id: 'timeline-test', diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index 6bf2b5e2a391e..86bb49fac7f3e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -6,7 +6,7 @@ import React, { useMemo, useRef } from 'react'; -import { BrowserFields } from '../../../../common/containers/source'; +import { BrowserFields, DocValueFields } from '../../../../common/containers/source'; import { TimelineItem, TimelineNonEcsData } from '../../../../graphql/types'; import { Note } from '../../../../common/lib/note'; import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model'; @@ -40,6 +40,7 @@ export interface BodyProps { columnHeaders: ColumnHeaderOptions[]; columnRenderers: ColumnRenderer[]; data: TimelineItem[]; + docValueFields: DocValueFields[]; getNotesByIds: (noteIds: string[]) => Note[]; graphEventId?: string; height?: number; @@ -75,6 +76,7 @@ export const Body = React.memo( columnHeaders, columnRenderers, data, + docValueFields, eventIdToNoteIds, getNotesByIds, graphEventId, @@ -183,6 +185,7 @@ export const Body = React.memo( columnHeaders={columnHeaders} columnRenderers={columnRenderers} data={data} + docValueFields={docValueFields} eventIdToNoteIds={eventIdToNoteIds} getNotesByIds={getNotesByIds} id={id} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx index 141534f1dcb6f..70971408e5003 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx @@ -11,7 +11,7 @@ import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import { RowRendererId, TimelineId } from '../../../../../common/types/timeline'; -import { BrowserFields } from '../../../../common/containers/source'; +import { BrowserFields, DocValueFields } from '../../../../common/containers/source'; import { TimelineItem } from '../../../../graphql/types'; import { Note } from '../../../../common/lib/note'; import { appSelectors, State } from '../../../../common/store'; @@ -41,6 +41,7 @@ import { plainRowRenderer } from './renderers/plain_row_renderer'; interface OwnProps { browserFields: BrowserFields; data: TimelineItem[]; + docValueFields: DocValueFields[]; height?: number; id: string; isEventViewer?: boolean; @@ -59,6 +60,7 @@ const StatefulBodyComponent = React.memo( browserFields, columnHeaders, data, + docValueFields, eventIdToNoteIds, excludedRowRendererIds, height, @@ -192,6 +194,7 @@ const StatefulBodyComponent = React.memo( columnHeaders={columnHeaders || emptyColumnHeaders} columnRenderers={columnRenderers} data={data} + docValueFields={docValueFields} eventIdToNoteIds={eventIdToNoteIds} getNotesByIds={getNotesByIds} graphEventId={graphEventId} @@ -225,6 +228,7 @@ const StatefulBodyComponent = React.memo( deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && deepEqual(prevProps.data, nextProps.data) && deepEqual(prevProps.excludedRowRendererIds, nextProps.excludedRowRendererIds) && + deepEqual(prevProps.docValueFields, nextProps.docValueFields) && prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && prevProps.graphEventId === nextProps.graphEventId && deepEqual(prevProps.notesById, nextProps.notesById) && diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx index 391d367ad3dc3..c371d1862be72 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx @@ -14,8 +14,8 @@ import { mockBrowserFields } from '../../../common/containers/source/mock'; import { EsQueryConfig, Filter, esFilters } from '../../../../../../../src/plugins/data/public'; const cleanUpKqlQuery = (str: string) => str.replace(/\n/g, '').replace(/\s\s+/g, ' '); -const startDate = new Date('2018-03-23T18:49:23.132Z').valueOf(); -const endDate = new Date('2018-03-24T03:33:52.253Z').valueOf(); +const startDate = '2018-03-23T18:49:23.132Z'; +const endDate = '2018-03-24T03:33:52.253Z'; describe('Build KQL Query', () => { test('Build KQL query with one data provider', () => { @@ -54,6 +54,14 @@ describe('Build KQL Query', () => { expect(cleanUpKqlQuery(kqlQuery)).toEqual('@timestamp: 1521848183232'); }); + test('Buld KQL query with one data provider as timestamp (numeric input as string)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = '@timestamp'; + dataProviders[0].queryMatch.value = '1521848183232'; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('@timestamp: 1521848183232'); + }); + test('Build KQL query with one data provider as date type (string input)', () => { const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); dataProviders[0].queryMatch.field = 'event.end'; @@ -70,6 +78,14 @@ describe('Build KQL Query', () => { expect(cleanUpKqlQuery(kqlQuery)).toEqual('event.end: 1521848183232'); }); + test('Buld KQL query with one data provider as date type (numeric input as string)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = 'event.end'; + dataProviders[0].queryMatch.value = '1521848183232'; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('event.end: 1521848183232'); + }); + test('Build KQL query with two data provider', () => { const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); @@ -244,8 +260,7 @@ describe('Combined Queries', () => { isEventViewer, }) ).toEqual({ - filterQuery: - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}', + filterQuery: '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', }); }); @@ -291,7 +306,7 @@ describe('Combined Queries', () => { }) ).toEqual({ filterQuery: - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}},{"exists":{"field":"host.name"}}],"should":[],"must_not":[]}}', + '{"bool":{"must":[],"filter":[{"match_all":{}},{"exists":{"field":"host.name"}}],"should":[],"must_not":[]}}', }); }); @@ -309,7 +324,7 @@ describe('Combined Queries', () => { end: endDate, })!; expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}' ); }); @@ -329,7 +344,7 @@ describe('Combined Queries', () => { end: endDate, })!; expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521848183232,"lte":1521848183232}}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521848183232,"lte":1521848183232}}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}' ); }); @@ -349,7 +364,7 @@ describe('Combined Queries', () => { end: endDate, })!; expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521848183232,"lte":1521848183232}}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521848183232,"lte":1521848183232}}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}' ); }); @@ -369,7 +384,7 @@ describe('Combined Queries', () => { end: endDate, })!; expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match":{"event.end":1521848183232}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"should":[{"match":{"event.end":1521848183232}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}' ); }); @@ -389,7 +404,7 @@ describe('Combined Queries', () => { end: endDate, })!; expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match":{"event.end":1521848183232}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"should":[{"match":{"event.end":1521848183232}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}' ); }); @@ -406,7 +421,7 @@ describe('Combined Queries', () => { end: endDate, })!; expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}' ); }); @@ -424,7 +439,7 @@ describe('Combined Queries', () => { end: endDate, })!; expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"should":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}' ); }); @@ -442,7 +457,7 @@ describe('Combined Queries', () => { end: endDate, })!; expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}' ); }); @@ -462,7 +477,7 @@ describe('Combined Queries', () => { end: endDate, })!; expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 3"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 4"}}],"minimum_should_match":1}}]}}]}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 2"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 5"}}],"minimum_should_match":1}}]}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"should":[{"bool":{"should":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 3"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 4"}}],"minimum_should_match":1}}]}}]}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 2"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 5"}}],"minimum_should_match":1}}]}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}' ); }); @@ -482,7 +497,7 @@ describe('Combined Queries', () => { end: endDate, })!; expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 3"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 4"}}],"minimum_should_match":1}}]}}]}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 2"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 5"}}],"minimum_should_match":1}}]}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 3"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 4"}}],"minimum_should_match":1}}]}}]}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 2"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 5"}}],"minimum_should_match":1}}]}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}' ); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx index a0087ab638dbf..b21ea3e4f86e9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEmpty, isNumber, get } from 'lodash/fp'; +import { isEmpty, get } from 'lodash/fp'; import memoizeOne from 'memoize-one'; import { escapeQueryValue, convertToBuildEsQuery } from '../../../common/lib/keury'; @@ -23,6 +23,8 @@ import { Filter, } from '../../../../../../../src/plugins/data/public'; +const isNumber = (value: string | number) => !isNaN(Number(value)); + const convertDateFieldToQuery = (field: string, value: string | number) => `${field}: ${isNumber(value) ? value : new Date(value).valueOf()}`; @@ -113,33 +115,28 @@ export const combineQueries = ({ filters: Filter[]; kqlQuery: Query; kqlMode: string; - start: number; - end: number; + start: string; + end: string; isEventViewer?: boolean; }): { filterQuery: string } | null => { const kuery: Query = { query: '', language: kqlQuery.language }; if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEmpty(filters) && !isEventViewer) { return null; } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEventViewer) { - kuery.query = `@timestamp >= ${start} and @timestamp <= ${end}`; return { filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), }; } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && !isEmpty(filters)) { - kuery.query = `@timestamp >= ${start} and @timestamp <= ${end}`; return { filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), }; } else if (isEmpty(dataProviders) && !isEmpty(kqlQuery.query)) { - kuery.query = `(${kqlQuery.query}) and @timestamp >= ${start} and @timestamp <= ${end}`; + kuery.query = `(${kqlQuery.query})`; return { filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), }; } else if (!isEmpty(dataProviders) && isEmpty(kqlQuery)) { - kuery.query = `(${buildGlobalQuery( - dataProviders, - browserFields - )}) and @timestamp >= ${start} and @timestamp <= ${end}`; + kuery.query = `(${buildGlobalQuery(dataProviders, browserFields)})`; return { filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), }; @@ -148,7 +145,7 @@ export const combineQueries = ({ const postpend = (q: string) => `${!isEmpty(q) ? ` ${operatorKqlQuery} (${q})` : ''}`; kuery.query = `((${buildGlobalQuery(dataProviders, browserFields)})${postpend( kqlQuery.query as string - )}) and @timestamp >= ${start} and @timestamp <= ${end}`; + )})`; return { filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx index 50a7782012b76..ce96e4e50dea0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx @@ -35,6 +35,8 @@ jest.mock('../../../common/lib/kibana', () => { }; }); +jest.mock('../../../common/components/url_state/normalize_time_range.ts'); + const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; jest.mock('use-resize-observer/polyfilled'); mockUseResizeObserver.mockImplementation(() => ({})); @@ -56,8 +58,8 @@ describe('StatefulTimeline', () => { columnId: '@timestamp', sortDirection: Direction.desc, }; - const startDate = new Date('2018-03-23T18:49:23.132Z').valueOf(); - const endDate = new Date('2018-03-24T03:33:52.253Z').valueOf(); + const startDate = '2018-03-23T18:49:23.132Z'; + const endDate = '2018-03-24T03:33:52.253Z'; const mocks = [ { request: { query: timelineQuery }, result: { data: { events: mockTimelineData } } }, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index c4d89fa29cb32..2d7527d8a922c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -171,13 +171,17 @@ const StatefulTimelineComponent = React.memo( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const { indexPattern, browserFields } = useWithSource('default', indexToAdd); + const { docValueFields, indexPattern, browserFields, loading: isLoadingSource } = useWithSource( + 'default', + indexToAdd + ); return ( ( indexPattern={indexPattern} indexToAdd={indexToAdd} isLive={isLive} + isLoadingSource={isLoadingSource} isSaving={isSaving} itemsPerPage={itemsPerPage!} itemsPerPageOptions={itemsPerPageOptions!} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx index 546f06b60cb56..75f684c629c70 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx @@ -65,9 +65,9 @@ describe('Timeline QueryBar ', () => { filterManager={new FilterManager(mockUiSettingsForFilterManager)} filterQuery={{ expression: 'here: query', kind: 'kuery' }} filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} - from={0} + from={'2020-07-07T08:20:18.966Z'} fromStr={DEFAULT_FROM} - to={1} + to={'2020-07-08T08:20:18.966Z'} toStr={DEFAULT_TO} kqlMode="search" indexPattern={mockIndexPattern} @@ -107,9 +107,9 @@ describe('Timeline QueryBar ', () => { filterManager={new FilterManager(mockUiSettingsForFilterManager)} filterQuery={{ expression: 'here: query', kind: 'kuery' }} filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} - from={0} + from={'2020-07-07T08:20:18.966Z'} fromStr={DEFAULT_FROM} - to={1} + to={'2020-07-08T08:20:18.966Z'} toStr={DEFAULT_TO} kqlMode="search" indexPattern={mockIndexPattern} @@ -154,9 +154,9 @@ describe('Timeline QueryBar ', () => { filterManager={new FilterManager(mockUiSettingsForFilterManager)} filterQuery={{ expression: 'here: query', kind: 'kuery' }} filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} - from={0} + from={'2020-07-07T08:20:18.966Z'} fromStr={DEFAULT_FROM} - to={1} + to={'2020-07-08T08:20:18.966Z'} toStr={DEFAULT_TO} kqlMode="search" indexPattern={mockIndexPattern} @@ -199,9 +199,9 @@ describe('Timeline QueryBar ', () => { filterManager={new FilterManager(mockUiSettingsForFilterManager)} filterQuery={{ expression: 'here: query', kind: 'kuery' }} filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} - from={0} + from={'2020-07-07T08:20:18.966Z'} fromStr={DEFAULT_FROM} - to={1} + to={'2020-07-08T08:20:18.966Z'} toStr={DEFAULT_TO} kqlMode="search" indexPattern={mockIndexPattern} @@ -246,9 +246,9 @@ describe('Timeline QueryBar ', () => { filterManager={new FilterManager(mockUiSettingsForFilterManager)} filterQuery={{ expression: 'here: query', kind: 'kuery' }} filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} - from={0} + from={'2020-07-07T08:20:18.966Z'} fromStr={DEFAULT_FROM} - to={1} + to={'2020-07-08T08:20:18.966Z'} toStr={DEFAULT_TO} kqlMode="search" indexPattern={mockIndexPattern} @@ -291,9 +291,9 @@ describe('Timeline QueryBar ', () => { filterManager={new FilterManager(mockUiSettingsForFilterManager)} filterQuery={{ expression: 'here: query', kind: 'kuery' }} filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} - from={0} + from={'2020-07-07T08:20:18.966Z'} fromStr={DEFAULT_FROM} - to={1} + to={'2020-07-08T08:20:18.966Z'} toStr={DEFAULT_TO} kqlMode="search" indexPattern={mockIndexPattern} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx index 967c5818a8722..74f21fecd0fda 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx @@ -37,7 +37,7 @@ export interface QueryBarTimelineComponentProps { filterManager: FilterManager; filterQuery: KueryFilterQuery; filterQueryDraft: KueryFilterQuery; - from: number; + from: string; fromStr: string; kqlMode: KqlMode; indexPattern: IIndexPattern; @@ -48,7 +48,7 @@ export interface QueryBarTimelineComponentProps { setKqlFilterQueryDraft: (expression: string, kind: KueryFilterQueryKind) => void; setSavedQueryId: (savedQueryId: string | null) => void; timelineId: string; - to: number; + to: string; toStr: string; updateReduxTime: DispatchUpdateReduxTime; } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx index 4d90bd875efcc..e04cef4ad8d93 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx @@ -51,7 +51,7 @@ interface Props { filterManager: FilterManager; filterQuery: KueryFilterQuery; filterQueryDraft: KueryFilterQuery; - from: number; + from: string; fromStr: string; indexPattern: IIndexPattern; isRefreshPaused: boolean; @@ -64,7 +64,7 @@ interface Props { setSavedQueryId: (savedQueryId: string | null) => void; filters: Filter[]; savedQueryId: string | null; - to: number; + to: string; toStr: string; updateEventType: (eventType: EventType) => void; updateReduxTime: DispatchUpdateReduxTime; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx index 7711cb7ba620e..58c46af5606f4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx @@ -59,8 +59,8 @@ describe('Timeline', () => { columnId: '@timestamp', sortDirection: Direction.desc, }; - const startDate = new Date('2018-03-23T18:49:23.132Z').valueOf(); - const endDate = new Date('2018-03-24T03:33:52.253Z').valueOf(); + const startDate = '2018-03-23T18:49:23.132Z'; + const endDate = '2018-03-24T03:33:52.253Z'; const indexPattern = mockIndexPattern; @@ -76,12 +76,14 @@ describe('Timeline', () => { columns: defaultHeaders, id: 'foo', dataProviders: mockDataProviders, + docValueFields: [], end: endDate, eventType: 'raw' as TimelineComponentProps['eventType'], filters: [], indexPattern, indexToAdd: [], isLive: false, + isLoadingSource: false, isSaving: false, itemsPerPage: 5, itemsPerPageOptions: [5, 10, 20], @@ -155,6 +157,42 @@ describe('Timeline', () => { expect(wrapper.find('[data-test-subj="events-table"]').exists()).toEqual(true); }); + test('it does NOT render the timeline table when the source is loading', () => { + const wrapper = mount( + + + + + + ); + + expect(wrapper.find('[data-test-subj="events-table"]').exists()).toEqual(false); + }); + + test('it does NOT render the timeline table when start is empty', () => { + const wrapper = mount( + + + + + + ); + + expect(wrapper.find('[data-test-subj="events-table"]').exists()).toEqual(false); + }); + + test('it does NOT render the timeline table when end is empty', () => { + const wrapper = mount( + + + + + + ); + + expect(wrapper.find('[data-test-subj="events-table"]').exists()).toEqual(false); + }); + test('it does NOT render the paging footer when you do NOT have any data providers', () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx index c1e97dcaef86a..c27af94addeab 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx @@ -11,7 +11,7 @@ import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import { FlyoutHeaderWithCloseButton } from '../flyout/header_with_close_button'; -import { BrowserFields } from '../../../common/containers/source'; +import { BrowserFields, DocValueFields } from '../../../common/containers/source'; import { TimelineQuery } from '../../containers/index'; import { Direction } from '../../../graphql/types'; import { useKibana } from '../../../common/lib/kibana'; @@ -98,7 +98,8 @@ export interface Props { browserFields: BrowserFields; columns: ColumnHeaderOptions[]; dataProviders: DataProvider[]; - end: number; + docValueFields: DocValueFields[]; + end: string; eventType?: EventType; filters: Filter[]; graphEventId?: string; @@ -106,6 +107,7 @@ export interface Props { indexPattern: IIndexPattern; indexToAdd: string[]; isLive: boolean; + isLoadingSource: boolean; isSaving: boolean; itemsPerPage: number; itemsPerPageOptions: number[]; @@ -121,7 +123,7 @@ export interface Props { onToggleDataProviderType: OnToggleDataProviderType; show: boolean; showCallOutUnauthorizedMsg: boolean; - start: number; + start: string; sort: Sort; status: TimelineStatusLiteral; toggleColumn: (column: ColumnHeaderOptions) => void; @@ -134,6 +136,7 @@ export const TimelineComponent: React.FC = ({ browserFields, columns, dataProviders, + docValueFields, end, eventType, filters, @@ -142,6 +145,7 @@ export const TimelineComponent: React.FC = ({ indexPattern, indexToAdd, isLive, + isLoadingSource, isSaving, itemsPerPage, itemsPerPageOptions, @@ -167,17 +171,47 @@ export const TimelineComponent: React.FC = ({ const dispatch = useDispatch(); const kibana = useKibana(); const [filterManager] = useState(new FilterManager(kibana.services.uiSettings)); - const combinedQueries = combineQueries({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - dataProviders, - indexPattern, - browserFields, - filters, - kqlQuery: { query: kqlQueryExpression, language: 'kuery' }, - kqlMode, - start, - end, - }); + const esQueryConfig = useMemo(() => esQuery.getEsQueryConfig(kibana.services.uiSettings), [ + kibana.services.uiSettings, + ]); + const kqlQuery = useMemo(() => ({ query: kqlQueryExpression, language: 'kuery' }), [ + kqlQueryExpression, + ]); + const combinedQueries = useMemo( + () => + combineQueries({ + config: esQueryConfig, + dataProviders, + indexPattern, + browserFields, + filters, + kqlQuery, + kqlMode, + start, + end, + }), + [ + browserFields, + dataProviders, + esQueryConfig, + start, + end, + filters, + indexPattern, + kqlMode, + kqlQuery, + ] + ); + + const canQueryTimeline = useMemo( + () => + combinedQueries != null && + isLoadingSource != null && + !isLoadingSource && + !isEmpty(start) && + !isEmpty(end), + [isLoadingSource, combinedQueries, start, end] + ); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; const timelineQueryFields = useMemo(() => columnsHeader.map((c) => c.id), [columnsHeader]); const timelineQuerySortField = useMemo( @@ -239,16 +273,19 @@ export const TimelineComponent: React.FC = ({ - {combinedQueries != null ? ( + {canQueryTimeline ? ( {({ events, @@ -277,6 +314,7 @@ export const TimelineComponent: React.FC = ({ React.ReactElement; + docValueFields: DocValueFields[]; indexName: string; eventId: string; executeQuery: boolean; @@ -34,12 +36,14 @@ const getDetailsEvent = memoizeOne( const TimelineDetailsQueryComponent: React.FC = ({ children, + docValueFields, indexName, eventId, executeQuery, sourceId, }) => { const variables: GetTimelineDetailsQuery.Variables = { + docValueFields, sourceId, indexName, eventId, diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts index 6c90b39a8e688..5a162fd2206a1 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts @@ -15,6 +15,8 @@ export const timelineQuery = gql` $filterQuery: String $defaultIndex: [String!]! $inspect: Boolean! + $docValueFields: [docValueFieldsInput!]! + $timerange: TimerangeInput! ) { source(id: $sourceId) { id @@ -24,6 +26,8 @@ export const timelineQuery = gql` sortField: $sortField filterQuery: $filterQuery defaultIndex: $defaultIndex + docValueFields: $docValueFields + timerange: $timerange ) { totalCount inspect @include(if: $inspect) { diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 164d34db16d87..510d58dbe6a69 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -49,6 +49,7 @@ export interface CustomReduxProps { export interface OwnProps extends QueryTemplateProps { children?: (args: TimelineArgs) => React.ReactNode; + endDate: string; eventType?: EventType; id: string; indexPattern?: IIndexPattern; @@ -56,6 +57,7 @@ export interface OwnProps extends QueryTemplateProps { limit: number; sortField: SortField; fields: string[]; + startDate: string; } type TimelineQueryProps = OwnProps & PropsFromRedux & WithKibanaProps & CustomReduxProps; @@ -77,6 +79,8 @@ class TimelineQueryComponent extends QueryTemplate< const { children, clearSignalsState, + docValueFields, + endDate, eventType = 'raw', id, indexPattern, @@ -88,6 +92,7 @@ class TimelineQueryComponent extends QueryTemplate< filterQuery, sourceId, sortField, + startDate, } = this.props; const defaultKibanaIndex = kibana.services.uiSettings.get(DEFAULT_INDEX_KEY); const defaultIndex = @@ -101,9 +106,15 @@ class TimelineQueryComponent extends QueryTemplate< fieldRequested: fields, filterQuery: createFilter(filterQuery), sourceId, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, pagination: { limit, cursor: null, tiebreaker: null }, sortField, defaultIndex, + docValueFields: docValueFields ?? [], inspect: isInspected, }; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index 618de48091ce8..faeef432ea422 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -56,8 +56,8 @@ export const createTimeline = actionCreator<{ id: string; dataProviders?: DataProvider[]; dateRange?: { - start: number; - end: number; + start: string; + end: string; }; excludedRowRendererIds?: RowRendererId[]; filters?: Filter[]; @@ -209,7 +209,7 @@ export const updateProviders = actionCreator<{ id: string; providers: DataProvid 'UPDATE_PROVIDERS' ); -export const updateRange = actionCreator<{ id: string; start: number; end: number }>( +export const updateRange = actionCreator<{ id: string; start: string; end: string }>( 'UPDATE_RANGE' ); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts index f4c4085715af9..7980f62cff171 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts @@ -9,11 +9,16 @@ import { TimelineType, TimelineStatus } from '../../../../common/types/timeline' import { Direction } from '../../../graphql/types'; import { DEFAULT_TIMELINE_WIDTH } from '../../components/timeline/body/constants'; import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers'; +import { normalizeTimeRange } from '../../../common/components/url_state/normalize_time_range'; import { SubsetTimelineModel, TimelineModel } from './model'; +// normalizeTimeRange uses getTimeRangeSettings which cannot be used outside Kibana context if the uiSettings is not false +const { from: start, to: end } = normalizeTimeRange({ from: '', to: '' }, false); + export const timelineDefaults: SubsetTimelineModel & Pick = { columns: defaultHeaders, dataProviders: [], + dateRange: { start, end }, deletedEventIds: [], description: '', eventType: 'all', @@ -42,10 +47,6 @@ export const timelineDefaults: SubsetTimelineModel & Pick { noteIds: [], pinnedEventIds: {}, pinnedEventsSaveObject: {}, - dateRange: { start: 1572469587644, end: 1572555987644 }, + dateRange: { start: '2019-10-30T21:06:27.644Z', end: '2019-10-31T21:06:27.644Z' }, savedObjectId: '11169110-fc22-11e9-8ca9-072f15ce2685', selectedEventIds: {}, show: true, @@ -158,9 +158,9 @@ describe('Epic Timeline', () => { expect( convertTimelineAsInput(timelineModel, { kind: 'absolute', - from: 1572469587644, + from: '2019-10-30T21:06:27.644Z', fromStr: undefined, - to: 1572555987644, + to: '2019-10-31T21:06:27.644Z', toStr: undefined, }) ).toEqual({ @@ -228,8 +228,8 @@ describe('Epic Timeline', () => { }, ], dateRange: { - end: 1572555987644, - start: 1572469587644, + end: '2019-10-31T21:06:27.644Z', + start: '2019-10-30T21:06:27.644Z', }, description: '', eventType: 'all', diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx index 7d65181db65fd..bd1fac9b05474 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx @@ -65,8 +65,8 @@ describe('epicLocalStorage', () => { columnId: '@timestamp', sortDirection: Direction.desc, }; - const startDate = new Date('2018-03-23T18:49:23.132Z').valueOf(); - const endDate = new Date('2018-03-24T03:33:52.253Z').valueOf(); + const startDate = '2018-03-23T18:49:23.132Z'; + const endDate = '2018-03-24T03:33:52.253Z'; const indexPattern = mockIndexPattern; @@ -83,12 +83,14 @@ describe('epicLocalStorage', () => { columns: defaultHeaders, id: 'foo', dataProviders: mockDataProviders, + docValueFields: [], end: endDate, eventType: 'raw' as TimelineComponentProps['eventType'], filters: [], indexPattern, indexToAdd: [], isLive: false, + isLoadingSource: false, isSaving: false, itemsPerPage: 5, itemsPerPageOptions: [5, 10, 20], diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts index 59f47297b1f65..2d16892329e19 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts @@ -26,6 +26,7 @@ import { TimelineType, RowRendererId, } from '../../../../common/types/timeline'; +import { normalizeTimeRange } from '../../../common/components/url_state/normalize_time_range'; import { timelineDefaults } from './defaults'; import { ColumnHeaderOptions, KqlMode, TimelineModel, EventType } from './model'; @@ -131,8 +132,8 @@ interface AddNewTimelineParams { columns: ColumnHeaderOptions[]; dataProviders?: DataProvider[]; dateRange?: { - start: number; - end: number; + start: string; + end: string; }; excludedRowRendererIds?: RowRendererId[]; filters?: Filter[]; @@ -153,7 +154,7 @@ interface AddNewTimelineParams { export const addNewTimeline = ({ columns, dataProviders = [], - dateRange = { start: 0, end: 0 }, + dateRange: mayDateRange, excludedRowRendererIds = [], filters = timelineDefaults.filters, id, @@ -165,6 +166,8 @@ export const addNewTimeline = ({ timelineById, timelineType, }: AddNewTimelineParams): TimelineById => { + const { from: startDateRange, to: endDateRange } = normalizeTimeRange({ from: '', to: '' }); + const dateRange = mayDateRange ?? { start: startDateRange, end: endDateRange }; const templateTimelineInfo = timelineType === TimelineType.template ? { @@ -752,8 +755,8 @@ export const updateTimelineProviders = ({ interface UpdateTimelineRangeParams { id: string; - start: number; - end: number; + start: string; + end: string; timelineById: TimelineById; } diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts index 95d525c7eb59f..9a8399d366967 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts @@ -101,8 +101,8 @@ export interface TimelineModel { pinnedEventsSaveObject: Record; /** Specifies the granularity of the date range (e.g. 1 Day / Week / Month) applicable to the mini-map */ dateRange: { - start: number; - end: number; + start: string; + end: string; }; savedQueryId?: string | null; /** Events selected on this timeline -- eventId to TimelineNonEcsData[] mapping of data required for batch actions **/ diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts index 4cfc20eb81705..0197ccc7eec05 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts @@ -48,6 +48,8 @@ import { ColumnHeaderOptions } from './model'; import { timelineDefaults } from './defaults'; import { TimelineById } from './types'; +jest.mock('../../../common/components/url_state/normalize_time_range.ts'); + const timelineByIdMock: TimelineById = { foo: { dataProviders: [ @@ -92,8 +94,8 @@ const timelineByIdMock: TimelineById = { pinnedEventIds: {}, pinnedEventsSaveObject: {}, dateRange: { - start: 0, - end: 0, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', }, selectedEventIds: {}, show: true, @@ -1009,8 +1011,8 @@ describe('Timeline', () => { test('should return a new reference and not the same reference', () => { const update = updateTimelineRange({ id: 'foo', - start: 23, - end: 33, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', timelineById: timelineByIdMock, }); expect(update).not.toBe(timelineByIdMock); @@ -1019,16 +1021,16 @@ describe('Timeline', () => { test('should update the timeline range', () => { const update = updateTimelineRange({ id: 'foo', - start: 23, - end: 33, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', timelineById: timelineByIdMock, }); expect(update).toEqual( set( 'foo.dateRange', { - start: 23, - end: 33, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', }, timelineByIdMock ) @@ -1135,8 +1137,8 @@ describe('Timeline', () => { templateTimelineId: null, noteIds: [], dateRange: { - start: 0, - end: 0, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', }, selectedEventIds: {}, show: true, @@ -1231,8 +1233,8 @@ describe('Timeline', () => { templateTimelineId: null, noteIds: [], dateRange: { - start: 0, - end: 0, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', }, selectedEventIds: {}, show: true, @@ -1437,8 +1439,8 @@ describe('Timeline', () => { templateTimelineId: null, noteIds: [], dateRange: { - start: 0, - end: 0, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', }, selectedEventIds: {}, show: true, @@ -1533,8 +1535,8 @@ describe('Timeline', () => { templateTimelineVersion: null, noteIds: [], dateRange: { - start: 0, - end: 0, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', }, selectedEventIds: {}, show: true, @@ -1635,8 +1637,8 @@ describe('Timeline', () => { templateTimelineId: null, noteIds: [], dateRange: { - start: 0, - end: 0, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', }, selectedEventIds: {}, show: true, @@ -1738,8 +1740,8 @@ describe('Timeline', () => { templateTimelineVersion: null, noteIds: [], dateRange: { - start: 0, - end: 0, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', }, selectedEventIds: {}, show: true, @@ -1933,8 +1935,8 @@ describe('Timeline', () => { templateTimelineId: null, noteIds: [], dateRange: { - start: 0, - end: 0, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', }, selectedEventIds: {}, show: true, @@ -2013,8 +2015,8 @@ describe('Timeline', () => { templateTimelineId: null, noteIds: [], dateRange: { - start: 0, - end: 0, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', }, selectedEventIds: {}, show: true, @@ -2117,8 +2119,8 @@ describe('Timeline', () => { templateTimelineId: null, noteIds: [], dateRange: { - start: 0, - end: 0, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', }, selectedEventIds: {}, show: true, diff --git a/x-pack/plugins/security_solution/server/graphql/authentications/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/authentications/schema.gql.ts index 20935ce9ed03f..648a65fa24682 100644 --- a/x-pack/plugins/security_solution/server/graphql/authentications/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/authentications/schema.gql.ts @@ -41,6 +41,7 @@ export const authenticationsSchema = gql` pagination: PaginationInputPaginated! filterQuery: String defaultIndex: [String!]! + docValueFields: [docValueFieldsInput!]! ): AuthenticationsData! } `; diff --git a/x-pack/plugins/security_solution/server/graphql/events/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/events/resolvers.ts index a9ef6bc682c84..ef28ac523ff85 100644 --- a/x-pack/plugins/security_solution/server/graphql/events/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/events/resolvers.ts @@ -58,6 +58,7 @@ export const createEventsResolvers = ( async LastEventTime(source, args, { req }) { const options: LastEventTimeRequestOptions = { defaultIndex: args.defaultIndex, + docValueFields: args.docValueFields, sourceConfiguration: source.configuration, indexKey: args.indexKey, details: args.details, diff --git a/x-pack/plugins/security_solution/server/graphql/events/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/events/schema.gql.ts index 3b71977bc0d47..eee4bc3e3a33f 100644 --- a/x-pack/plugins/security_solution/server/graphql/events/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/events/schema.gql.ts @@ -76,17 +76,20 @@ export const eventsSchema = gql` timerange: TimerangeInput filterQuery: String defaultIndex: [String!]! + docValueFields: [docValueFieldsInput!]! ): TimelineData! TimelineDetails( eventId: String! indexName: String! defaultIndex: [String!]! + docValueFields: [docValueFieldsInput!]! ): TimelineDetailsData! LastEventTime( id: String indexKey: LastEventIndexKey! details: LastTimeDetails! defaultIndex: [String!]! + docValueFields: [docValueFieldsInput!]! ): LastEventTimeData! } `; diff --git a/x-pack/plugins/security_solution/server/graphql/hosts/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/hosts/resolvers.ts index e37ade585e8be..181ee3c2b4e94 100644 --- a/x-pack/plugins/security_solution/server/graphql/hosts/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/hosts/resolvers.ts @@ -71,6 +71,7 @@ export const createHostsResolvers = ( sourceConfiguration: source.configuration, hostName: args.hostName, defaultIndex: args.defaultIndex, + docValueFields: args.docValueFields, }; return libs.hosts.getHostFirstLastSeen(req, options); }, diff --git a/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts index 02f8341cd6fd9..48bb0cbe37afd 100644 --- a/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts @@ -99,6 +99,7 @@ export const hostsSchema = gql` sort: HostsSortField! filterQuery: String defaultIndex: [String!]! + docValueFields: [docValueFieldsInput!]! ): HostsData! HostOverview( id: String @@ -106,6 +107,11 @@ export const hostsSchema = gql` timerange: TimerangeInput! defaultIndex: [String!]! ): HostItem! - HostFirstLastSeen(id: String, hostName: String!, defaultIndex: [String!]!): FirstLastSeenHost! + HostFirstLastSeen( + id: String + hostName: String! + defaultIndex: [String!]! + docValueFields: [docValueFieldsInput!]! + ): FirstLastSeenHost! } `; diff --git a/x-pack/plugins/security_solution/server/graphql/ip_details/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/ip_details/schema.gql.ts index 4684449c1b80f..2531f8d169327 100644 --- a/x-pack/plugins/security_solution/server/graphql/ip_details/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/ip_details/schema.gql.ts @@ -38,6 +38,7 @@ const ipOverviewSchema = gql` filterQuery: String ip: String! defaultIndex: [String!]! + docValueFields: [docValueFieldsInput!]! ): IpOverviewData } `; diff --git a/x-pack/plugins/security_solution/server/graphql/network/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/network/schema.gql.ts index 15e2d832a73c9..9bb8a48c12f0d 100644 --- a/x-pack/plugins/security_solution/server/graphql/network/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/network/schema.gql.ts @@ -238,6 +238,7 @@ export const networkSchema = gql` defaultIndex: [String!]! timerange: TimerangeInput! stackByField: String + docValueFields: [docValueFieldsInput!]! ): NetworkDsOverTimeData! NetworkHttp( id: String diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts index 7cbeea67b2750..fce81e2f0dce0 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts @@ -34,8 +34,8 @@ const kueryFilterQuery = ` `; const dateRange = ` - start: Float - end: Float + start: ToAny + end: ToAny `; const favoriteTimeline = ` diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index 1eaf47ad43812..f8a614e86f28e 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -26,9 +26,9 @@ export interface TimerangeInput { /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */ interval: string; /** The end of the timerange */ - to: number; + to: string; /** The beginning of the timerange */ - from: number; + from: string; } export interface PaginationInputPaginated { @@ -42,6 +42,12 @@ export interface PaginationInputPaginated { querySize: number; } +export interface DocValueFieldsInput { + field: string; + + format: string; +} + export interface PaginationInput { /** The limit parameter allows you to configure the maximum amount of items to be returned */ limit: number; @@ -262,9 +268,9 @@ export interface KueryFilterQueryInput { } export interface DateRangePickerInput { - start?: Maybe; + start?: Maybe; - end?: Maybe; + end?: Maybe; } export interface SortTimelineInput { @@ -2095,9 +2101,9 @@ export interface QueryMatchResult { } export interface DateRangePickerResult { - start?: Maybe; + start?: Maybe; - end?: Maybe; + end?: Maybe; } export interface FavoriteTimelineResult { @@ -2334,6 +2340,8 @@ export interface AuthenticationsSourceArgs { filterQuery?: Maybe; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface TimelineSourceArgs { pagination: PaginationInput; @@ -2347,6 +2355,8 @@ export interface TimelineSourceArgs { filterQuery?: Maybe; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface TimelineDetailsSourceArgs { eventId: string; @@ -2354,6 +2364,8 @@ export interface TimelineDetailsSourceArgs { indexName: string; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface LastEventTimeSourceArgs { id?: Maybe; @@ -2363,6 +2375,8 @@ export interface LastEventTimeSourceArgs { details: LastTimeDetails; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface HostsSourceArgs { id?: Maybe; @@ -2376,6 +2390,8 @@ export interface HostsSourceArgs { filterQuery?: Maybe; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface HostOverviewSourceArgs { id?: Maybe; @@ -2392,6 +2408,8 @@ export interface HostFirstLastSeenSourceArgs { hostName: string; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface IpOverviewSourceArgs { id?: Maybe; @@ -2401,6 +2419,8 @@ export interface IpOverviewSourceArgs { ip: string; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface UsersSourceArgs { filterQuery?: Maybe; @@ -2516,6 +2536,8 @@ export interface NetworkDnsHistogramSourceArgs { timerange: TimerangeInput; stackByField?: Maybe; + + docValueFields: DocValueFieldsInput[]; } export interface NetworkHttpSourceArgs { id?: Maybe; @@ -3054,6 +3076,8 @@ export namespace SourceResolvers { filterQuery?: Maybe; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export type TimelineResolver< @@ -3073,6 +3097,8 @@ export namespace SourceResolvers { filterQuery?: Maybe; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export type TimelineDetailsResolver< @@ -3086,6 +3112,8 @@ export namespace SourceResolvers { indexName: string; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export type LastEventTimeResolver< @@ -3101,6 +3129,8 @@ export namespace SourceResolvers { details: LastTimeDetails; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export type HostsResolver = Resolver< @@ -3121,6 +3151,8 @@ export namespace SourceResolvers { filterQuery?: Maybe; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export type HostOverviewResolver< @@ -3149,6 +3181,8 @@ export namespace SourceResolvers { hostName: string; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export type IpOverviewResolver< @@ -3164,6 +3198,8 @@ export namespace SourceResolvers { ip: string; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export type UsersResolver = Resolver< @@ -3334,6 +3370,8 @@ export namespace SourceResolvers { timerange: TimerangeInput; stackByField?: Maybe; + + docValueFields: DocValueFieldsInput[]; } export type NetworkHttpResolver< @@ -8559,18 +8597,18 @@ export namespace QueryMatchResultResolvers { export namespace DateRangePickerResultResolvers { export interface Resolvers { - start?: StartResolver, TypeParent, TContext>; + start?: StartResolver, TypeParent, TContext>; - end?: EndResolver, TypeParent, TContext>; + end?: EndResolver, TypeParent, TContext>; } export type StartResolver< - R = Maybe, + R = Maybe, Parent = DateRangePickerResult, TContext = SiemContext > = Resolver; export type EndResolver< - R = Maybe, + R = Maybe, Parent = DateRangePickerResult, TContext = SiemContext > = Resolver; diff --git a/x-pack/plugins/security_solution/server/lib/authentications/query.dsl.ts b/x-pack/plugins/security_solution/server/lib/authentications/query.dsl.ts index b9ed88e91f87d..b6b72cd37efaa 100644 --- a/x-pack/plugins/security_solution/server/lib/authentications/query.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/authentications/query.dsl.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEmpty } from 'lodash/fp'; + import { createQueryFilterClauses } from '../../utils/build_query'; import { reduceFields } from '../../utils/build_query/reduce_fields'; import { hostFieldsMap, sourceFieldsMap } from '../ecs_fields'; @@ -26,6 +28,7 @@ export const buildQuery = ({ timerange: { from, to }, pagination: { querySize }, defaultIndex, + docValueFields, sourceConfiguration: { fields: { timestamp }, }, @@ -40,6 +43,7 @@ export const buildQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, @@ -58,6 +62,7 @@ export const buildQuery = ({ index: defaultIndex, ignoreUnavailable: true, body: { + ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), aggregations: { ...agg, group_by_users: { diff --git a/x-pack/plugins/security_solution/server/lib/events/elasticsearch_adapter.ts b/x-pack/plugins/security_solution/server/lib/events/elasticsearch_adapter.ts index 6ad18c5578f93..aabb18d419098 100644 --- a/x-pack/plugins/security_solution/server/lib/events/elasticsearch_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/events/elasticsearch_adapter.ts @@ -84,7 +84,7 @@ export class ElasticsearchEventsAdapter implements EventsAdapter { request: FrameworkRequest, options: RequestDetailsOptions ): Promise { - const dsl = buildDetailsQuery(options.indexName, options.eventId); + const dsl = buildDetailsQuery(options.indexName, options.eventId, options.docValueFields ?? []); const searchResponse = await this.framework.callWithRequest( request, 'search', diff --git a/x-pack/plugins/security_solution/server/lib/events/query.dsl.ts b/x-pack/plugins/security_solution/server/lib/events/query.dsl.ts index bc95fe5629449..143ef1e9d5bf0 100644 --- a/x-pack/plugins/security_solution/server/lib/events/query.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/events/query.dsl.ts @@ -3,74 +3,15 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { isEmpty } from 'lodash/fp'; -import { SortField, TimerangeInput } from '../../graphql/types'; +import { SortField, TimerangeInput, DocValueFieldsInput } from '../../graphql/types'; import { createQueryFilterClauses } from '../../utils/build_query'; -import { RequestOptions, RequestOptionsPaginated } from '../framework'; +import { RequestOptions } from '../framework'; import { SortRequest } from '../types'; import { TimerangeFilter } from './types'; -export const buildQuery = (options: RequestOptionsPaginated) => { - const { querySize } = options.pagination; - const { fields, filterQuery } = options; - const filterClause = [...createQueryFilterClauses(filterQuery)]; - const defaultIndex = options.defaultIndex; - - const getTimerangeFilter = (timerange: TimerangeInput | undefined): TimerangeFilter[] => { - if (timerange) { - const { to, from } = timerange; - return [ - { - range: { - [options.sourceConfiguration.fields.timestamp]: { - gte: from, - lte: to, - }, - }, - }, - ]; - } - return []; - }; - - const filter = [...filterClause, ...getTimerangeFilter(options.timerange), { match_all: {} }]; - - const getSortField = (sortField: SortField) => { - if (sortField.sortFieldId) { - const field: string = - sortField.sortFieldId === 'timestamp' ? '@timestamp' : sortField.sortFieldId; - - return [ - { [field]: sortField.direction }, - { [options.sourceConfiguration.fields.tiebreaker]: sortField.direction }, - ]; - } - return []; - }; - - const sort: SortRequest = getSortField(options.sortField!); - - const dslQuery = { - allowNoIndices: true, - index: defaultIndex, - ignoreUnavailable: true, - body: { - query: { - bool: { - filter, - }, - }, - size: querySize, - track_total_hits: true, - sort, - _source: fields, - }, - }; - - return dslQuery; -}; - export const buildTimelineQuery = (options: RequestOptions) => { const { limit, cursor, tiebreaker } = options.pagination; const { fields, filterQuery } = options; @@ -86,6 +27,7 @@ export const buildTimelineQuery = (options: RequestOptions) => { [options.sourceConfiguration.fields.timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, @@ -116,6 +58,7 @@ export const buildTimelineQuery = (options: RequestOptions) => { index: defaultIndex, ignoreUnavailable: true, body: { + ...(isEmpty(options.docValueFields) ? { docvalue_fields: options.docValueFields } : {}), query: { bool: { filter, @@ -141,11 +84,16 @@ export const buildTimelineQuery = (options: RequestOptions) => { return dslQuery; }; -export const buildDetailsQuery = (indexName: string, id: string) => ({ +export const buildDetailsQuery = ( + indexName: string, + id: string, + docValueFields: DocValueFieldsInput[] +) => ({ allowNoIndices: true, index: indexName, ignoreUnavailable: true, body: { + docvalue_fields: docValueFields, query: { terms: { _id: [id], diff --git a/x-pack/plugins/security_solution/server/lib/events/query.last_event_time.dsl.ts b/x-pack/plugins/security_solution/server/lib/events/query.last_event_time.dsl.ts index 86491876673c9..6c443fed3c99d 100644 --- a/x-pack/plugins/security_solution/server/lib/events/query.last_event_time.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/events/query.last_event_time.dsl.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEmpty } from 'lodash/fp'; + import { LastEventTimeRequestOptions } from './types'; import { LastEventIndexKey } from '../../graphql/types'; import { assertUnreachable } from '../../utils/build_query'; @@ -16,6 +18,7 @@ export const buildLastEventTimeQuery = ({ indexKey, details, defaultIndex, + docValueFields, }: LastEventTimeRequestOptions) => { const indicesToQuery: EventIndices = { hosts: defaultIndex, @@ -35,6 +38,7 @@ export const buildLastEventTimeQuery = ({ index: indicesToQuery.network, ignoreUnavailable: true, body: { + ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), aggregations: { last_seen_event: { max: { field: '@timestamp' } }, }, @@ -52,6 +56,7 @@ export const buildLastEventTimeQuery = ({ index: indicesToQuery.hosts, ignoreUnavailable: true, body: { + ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), aggregations: { last_seen_event: { max: { field: '@timestamp' } }, }, @@ -69,6 +74,7 @@ export const buildLastEventTimeQuery = ({ index: indicesToQuery[indexKey], ignoreUnavailable: true, body: { + ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), aggregations: { last_seen_event: { max: { field: '@timestamp' } }, }, diff --git a/x-pack/plugins/security_solution/server/lib/events/types.ts b/x-pack/plugins/security_solution/server/lib/events/types.ts index 3a4a8705f7387..aae2360e42e65 100644 --- a/x-pack/plugins/security_solution/server/lib/events/types.ts +++ b/x-pack/plugins/security_solution/server/lib/events/types.ts @@ -11,6 +11,7 @@ import { SourceConfiguration, TimelineData, TimelineDetailsData, + DocValueFieldsInput, } from '../../graphql/types'; import { FrameworkRequest, RequestOptions, RequestOptionsPaginated } from '../framework'; import { SearchHit } from '../types'; @@ -61,13 +62,15 @@ export interface LastEventTimeRequestOptions { details: LastTimeDetails; sourceConfiguration: SourceConfiguration; defaultIndex: string[]; + docValueFields: DocValueFieldsInput[]; } export interface TimerangeFilter { range: { [timestamp: string]: { - gte: number; - lte: number; + gte: string; + lte: string; + format: string; }; }; } @@ -76,6 +79,7 @@ export interface RequestDetailsOptions { indexName: string; eventId: string; defaultIndex: string[]; + docValueFields?: DocValueFieldsInput[]; } interface EventsOverTimeHistogramData { diff --git a/x-pack/plugins/security_solution/server/lib/framework/types.ts b/x-pack/plugins/security_solution/server/lib/framework/types.ts index abe572df87063..03c82ceb02e68 100644 --- a/x-pack/plugins/security_solution/server/lib/framework/types.ts +++ b/x-pack/plugins/security_solution/server/lib/framework/types.ts @@ -18,6 +18,7 @@ import { TimerangeInput, Maybe, HistogramType, + DocValueFieldsInput, } from '../../graphql/types'; export * from '../../utils/typed_resolvers'; @@ -115,6 +116,7 @@ export interface RequestBasicOptions { timerange: TimerangeInput; filterQuery: ESQuery | undefined; defaultIndex: string[]; + docValueFields?: DocValueFieldsInput[]; } export interface MatrixHistogramRequestOptions extends RequestBasicOptions { diff --git a/x-pack/plugins/security_solution/server/lib/hosts/mock.ts b/x-pack/plugins/security_solution/server/lib/hosts/mock.ts index 0f6bc5c1b0e0c..44767563c6b75 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/mock.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/mock.ts @@ -24,7 +24,7 @@ export const mockGetHostsOptions: HostsRequestOptions = { timestamp: '@timestamp', }, }, - timerange: { interval: '12h', to: 1554824274610, from: 1554737874610 }, + timerange: { interval: '12h', to: '2019-04-09T15:37:54.610Z', from: '2019-04-08T15:37:54.610Z' }, sort: { field: HostsFields.lastSeen, direction: Direction.asc }, pagination: { activePage: 0, @@ -295,7 +295,7 @@ export const mockGetHostOverviewOptions: HostOverviewRequestOptions = { timestamp: '@timestamp', }, }, - timerange: { interval: '12h', to: 1554824274610, from: 1554737874610 }, + timerange: { interval: '12h', to: '2019-04-09T15:37:54.610Z', from: '2019-04-08T15:37:54.610Z' }, defaultIndex: DEFAULT_INDEX_PATTERN, fields: [ '_id', diff --git a/x-pack/plugins/security_solution/server/lib/hosts/query.hosts.dsl.ts b/x-pack/plugins/security_solution/server/lib/hosts/query.hosts.dsl.ts index 70f57769362f5..013afd5cd58f5 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/query.hosts.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/query.hosts.dsl.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEmpty } from 'lodash/fp'; + import { Direction, HostsFields, HostsSortField } from '../../graphql/types'; import { assertUnreachable, createQueryFilterClauses } from '../../utils/build_query'; @@ -11,6 +13,7 @@ import { HostsRequestOptions } from '.'; export const buildHostsQuery = ({ defaultIndex, + docValueFields, fields, filterQuery, pagination: { querySize }, @@ -27,6 +30,7 @@ export const buildHostsQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, @@ -39,6 +43,7 @@ export const buildHostsQuery = ({ index: defaultIndex, ignoreUnavailable: true, body: { + ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), aggregations: { ...agg, host_data: { diff --git a/x-pack/plugins/security_solution/server/lib/hosts/query.last_first_seen_host.dsl.ts b/x-pack/plugins/security_solution/server/lib/hosts/query.last_first_seen_host.dsl.ts index d7ab22100b246..3bdaee58917ea 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/query.last_first_seen_host.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/query.last_first_seen_host.dsl.ts @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEmpty } from 'lodash/fp'; import { HostLastFirstSeenRequestOptions } from './types'; export const buildLastFirstSeenHostQuery = ({ hostName, defaultIndex, + docValueFields, }: HostLastFirstSeenRequestOptions) => { const filter = [{ term: { 'host.name': hostName } }]; @@ -17,6 +19,7 @@ export const buildLastFirstSeenHostQuery = ({ index: defaultIndex, ignoreUnavailable: true, body: { + ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), aggregations: { firstSeen: { min: { field: '@timestamp' } }, lastSeen: { max: { field: '@timestamp' } }, diff --git a/x-pack/plugins/security_solution/server/lib/hosts/types.ts b/x-pack/plugins/security_solution/server/lib/hosts/types.ts index e52cfe9d7feeb..fc621f81a4f5f 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/types.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/types.ts @@ -14,6 +14,7 @@ import { OsEcsFields, SourceConfiguration, TimerangeInput, + DocValueFieldsInput, } from '../../graphql/types'; import { FrameworkRequest, RequestOptionsPaginated } from '../framework'; import { Hit, Hits, SearchHit, TotalValue } from '../types'; @@ -50,6 +51,7 @@ export interface HostLastFirstSeenRequestOptions { hostName: string; sourceConfiguration: SourceConfiguration; defaultIndex: string[]; + docValueFields?: DocValueFieldsInput[]; } export interface HostOverviewRequestOptions extends HostLastFirstSeenRequestOptions { diff --git a/x-pack/plugins/security_solution/server/lib/ip_details/query_overview.dsl.ts b/x-pack/plugins/security_solution/server/lib/ip_details/query_overview.dsl.ts index 5803b832a334b..d9c8f32d0b465 100644 --- a/x-pack/plugins/security_solution/server/lib/ip_details/query_overview.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/ip_details/query_overview.dsl.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEmpty } from 'lodash/fp'; import { IpOverviewRequestOptions } from './index'; const getAggs = (type: string, ip: string) => { @@ -95,12 +96,17 @@ const getHostAggs = (ip: string) => { }; }; -export const buildOverviewQuery = ({ defaultIndex, ip }: IpOverviewRequestOptions) => { +export const buildOverviewQuery = ({ + defaultIndex, + docValueFields, + ip, +}: IpOverviewRequestOptions) => { const dslQuery = { allowNoIndices: true, index: defaultIndex, ignoreUnavailable: true, body: { + ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), aggs: { ...getAggs('source', ip), ...getAggs('destination', ip), @@ -115,5 +121,6 @@ export const buildOverviewQuery = ({ defaultIndex, ip }: IpOverviewRequestOption track_total_hits: false, }, }; + return dslQuery; }; diff --git a/x-pack/plugins/security_solution/server/lib/ip_details/query_users.dsl.ts b/x-pack/plugins/security_solution/server/lib/ip_details/query_users.dsl.ts index b245332525694..10678dc033eb5 100644 --- a/x-pack/plugins/security_solution/server/lib/ip_details/query_users.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/ip_details/query_users.dsl.ts @@ -23,7 +23,11 @@ export const buildUsersQuery = ({ }: UsersRequestOptions) => { const filter = [ ...createQueryFilterClauses(filterQuery), - { range: { [timestamp]: { gte: from, lte: to } } }, + { + range: { + [timestamp]: { gte: from, lte: to, format: 'strict_date_optional_time' }, + }, + }, { term: { [`${flowTarget}.ip`]: ip } }, ]; diff --git a/x-pack/plugins/security_solution/server/lib/kpi_hosts/mock.ts b/x-pack/plugins/security_solution/server/lib/kpi_hosts/mock.ts index a5affea2842a6..876d2f9c16bed 100644 --- a/x-pack/plugins/security_solution/server/lib/kpi_hosts/mock.ts +++ b/x-pack/plugins/security_solution/server/lib/kpi_hosts/mock.ts @@ -7,8 +7,8 @@ import { DEFAULT_INDEX_PATTERN } from '../../../common/constants'; import { RequestBasicOptions } from '../framework/types'; -const FROM = new Date('2019-05-03T13:24:00.660Z').valueOf(); -const TO = new Date('2019-05-04T13:24:00.660Z').valueOf(); +const FROM = '2019-05-03T13:24:00.660Z'; +const TO = '2019-05-04T13:24:00.660Z'; export const mockKpiHostsOptions: RequestBasicOptions = { defaultIndex: DEFAULT_INDEX_PATTERN, diff --git a/x-pack/plugins/security_solution/server/lib/kpi_hosts/query_authentication.dsl.ts b/x-pack/plugins/security_solution/server/lib/kpi_hosts/query_authentication.dsl.ts index 0b7803d007194..ee9e6cd5a66c5 100644 --- a/x-pack/plugins/security_solution/server/lib/kpi_hosts/query_authentication.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/kpi_hosts/query_authentication.dsl.ts @@ -33,6 +33,7 @@ export const buildAuthQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/kpi_hosts/query_hosts.dsl.ts b/x-pack/plugins/security_solution/server/lib/kpi_hosts/query_hosts.dsl.ts index 87ebf0cf0e6e7..0c1d7d4ae9de7 100644 --- a/x-pack/plugins/security_solution/server/lib/kpi_hosts/query_hosts.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/kpi_hosts/query_hosts.dsl.ts @@ -22,6 +22,7 @@ export const buildHostsQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/kpi_hosts/query_unique_ips.dsl.ts b/x-pack/plugins/security_solution/server/lib/kpi_hosts/query_unique_ips.dsl.ts index 72833aaf9ea5b..9813f73101235 100644 --- a/x-pack/plugins/security_solution/server/lib/kpi_hosts/query_unique_ips.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/kpi_hosts/query_unique_ips.dsl.ts @@ -22,6 +22,7 @@ export const buildUniqueIpsQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/kpi_network/mock.ts b/x-pack/plugins/security_solution/server/lib/kpi_network/mock.ts index cc0849ccdf1d2..fc9b64ae0746f 100644 --- a/x-pack/plugins/security_solution/server/lib/kpi_network/mock.ts +++ b/x-pack/plugins/security_solution/server/lib/kpi_network/mock.ts @@ -19,7 +19,7 @@ export const mockOptions: RequestBasicOptions = { timestamp: '@timestamp', }, }, - timerange: { interval: '12h', to: 1549852006071, from: 1549765606071 }, + timerange: { interval: '12h', to: '2019-02-11T02:26:46.071Z', from: '2019-02-10T02:26:46.071Z' }, filterQuery: {}, }; @@ -28,7 +28,11 @@ export const mockRequest = { operationName: 'GetKpiNetworkQuery', variables: { sourceId: 'default', - timerange: { interval: '12h', from: 1557445721842, to: 1557532121842 }, + timerange: { + interval: '12h', + from: '2019-05-09T23:48:41.842Z', + to: '2019-05-10T23:48:41.842Z', + }, filterQuery: '', }, query: diff --git a/x-pack/plugins/security_solution/server/lib/kpi_network/query_dns.dsl.ts b/x-pack/plugins/security_solution/server/lib/kpi_network/query_dns.dsl.ts index 01771ad973b5d..b3dba9b1d0fab 100644 --- a/x-pack/plugins/security_solution/server/lib/kpi_network/query_dns.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/kpi_network/query_dns.dsl.ts @@ -51,6 +51,7 @@ export const buildDnsQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/kpi_network/query_network_events.ts b/x-pack/plugins/security_solution/server/lib/kpi_network/query_network_events.ts index 1a87aff047a25..17f705fe98d03 100644 --- a/x-pack/plugins/security_solution/server/lib/kpi_network/query_network_events.ts +++ b/x-pack/plugins/security_solution/server/lib/kpi_network/query_network_events.ts @@ -25,6 +25,7 @@ export const buildNetworkEventsQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/kpi_network/query_tls_handshakes.dsl.ts b/x-pack/plugins/security_solution/server/lib/kpi_network/query_tls_handshakes.dsl.ts index 09bc0eae642e4..5032863e7d324 100644 --- a/x-pack/plugins/security_solution/server/lib/kpi_network/query_tls_handshakes.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/kpi_network/query_tls_handshakes.dsl.ts @@ -51,6 +51,7 @@ export const buildTlsHandshakeQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/kpi_network/query_unique_flow.ts b/x-pack/plugins/security_solution/server/lib/kpi_network/query_unique_flow.ts index 4581b889cc9ef..fb717df2b4608 100644 --- a/x-pack/plugins/security_solution/server/lib/kpi_network/query_unique_flow.ts +++ b/x-pack/plugins/security_solution/server/lib/kpi_network/query_unique_flow.ts @@ -25,6 +25,7 @@ export const buildUniqueFlowIdsQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/kpi_network/query_unique_private_ips.dsl.ts b/x-pack/plugins/security_solution/server/lib/kpi_network/query_unique_private_ips.dsl.ts index f12ab2a3072ae..77d6efdcfdaa0 100644 --- a/x-pack/plugins/security_solution/server/lib/kpi_network/query_unique_private_ips.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/kpi_network/query_unique_private_ips.dsl.ts @@ -77,6 +77,7 @@ export const buildUniquePrvateIpQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/matrix_histogram/query.anomalies_over_time.dsl.ts b/x-pack/plugins/security_solution/server/lib/matrix_histogram/query.anomalies_over_time.dsl.ts index 38e8387f43ffd..fb4e666cda964 100644 --- a/x-pack/plugins/security_solution/server/lib/matrix_histogram/query.anomalies_over_time.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/matrix_histogram/query.anomalies_over_time.dsl.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment'; + import { createQueryFilterClauses, calculateTimeSeriesInterval } from '../../utils/build_query'; import { MatrixHistogramRequestOptions } from '../framework'; @@ -20,6 +22,7 @@ export const buildAnomaliesOverTimeQuery = ({ timestamp: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, @@ -34,8 +37,8 @@ export const buildAnomaliesOverTimeQuery = ({ fixed_interval: interval, min_doc_count: 0, extended_bounds: { - min: from, - max: to, + min: moment(from).valueOf(), + max: moment(to).valueOf(), }, }, }; diff --git a/x-pack/plugins/security_solution/server/lib/matrix_histogram/query.authentications_over_time.dsl.ts b/x-pack/plugins/security_solution/server/lib/matrix_histogram/query.authentications_over_time.dsl.ts index 34a3804f974de..174cc907214a9 100644 --- a/x-pack/plugins/security_solution/server/lib/matrix_histogram/query.authentications_over_time.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/matrix_histogram/query.authentications_over_time.dsl.ts @@ -3,6 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment'; + import { createQueryFilterClauses, calculateTimeSeriesInterval } from '../../utils/build_query'; import { MatrixHistogramRequestOptions } from '../framework'; @@ -33,6 +35,7 @@ export const buildAuthenticationsOverTimeQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, @@ -47,8 +50,8 @@ export const buildAuthenticationsOverTimeQuery = ({ fixed_interval: interval, min_doc_count: 0, extended_bounds: { - min: from, - max: to, + min: moment(from).valueOf(), + max: moment(to).valueOf(), }, }, }; diff --git a/x-pack/plugins/security_solution/server/lib/matrix_histogram/query.events_over_time.dsl.ts b/x-pack/plugins/security_solution/server/lib/matrix_histogram/query.events_over_time.dsl.ts index 63649a1064b02..fa7c1b9e55b9e 100644 --- a/x-pack/plugins/security_solution/server/lib/matrix_histogram/query.events_over_time.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/matrix_histogram/query.events_over_time.dsl.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment'; + import { showAllOthersBucket } from '../../../common/constants'; import { createQueryFilterClauses, calculateTimeSeriesInterval } from '../../utils/build_query'; import { MatrixHistogramRequestOptions } from '../framework'; @@ -26,6 +28,7 @@ export const buildEventsOverTimeQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, @@ -40,8 +43,8 @@ export const buildEventsOverTimeQuery = ({ fixed_interval: interval, min_doc_count: 0, extended_bounds: { - min: from, - max: to, + min: moment(from).valueOf(), + max: moment(to).valueOf(), }, }, }; diff --git a/x-pack/plugins/security_solution/server/lib/matrix_histogram/query_alerts.dsl.ts b/x-pack/plugins/security_solution/server/lib/matrix_histogram/query_alerts.dsl.ts index 4963f01d67a4f..dd45109672480 100644 --- a/x-pack/plugins/security_solution/server/lib/matrix_histogram/query_alerts.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/matrix_histogram/query_alerts.dsl.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment'; + import { createQueryFilterClauses, calculateTimeSeriesInterval } from '../../utils/build_query'; import { buildTimelineQuery } from '../events/query.dsl'; import { RequestOptions, MatrixHistogramRequestOptions } from '../framework'; @@ -62,6 +64,7 @@ export const buildAlertsHistogramQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, @@ -76,8 +79,8 @@ export const buildAlertsHistogramQuery = ({ fixed_interval: interval, min_doc_count: 0, extended_bounds: { - min: from, - max: to, + min: moment(from).valueOf(), + max: moment(to).valueOf(), }, }, }; diff --git a/x-pack/plugins/security_solution/server/lib/matrix_histogram/query_dns_histogram.dsl.ts b/x-pack/plugins/security_solution/server/lib/matrix_histogram/query_dns_histogram.dsl.ts index a6c75fe01eb15..7e71263988957 100644 --- a/x-pack/plugins/security_solution/server/lib/matrix_histogram/query_dns_histogram.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/matrix_histogram/query_dns_histogram.dsl.ts @@ -23,6 +23,7 @@ export const buildDnsHistogramQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/network/mock.ts b/x-pack/plugins/security_solution/server/lib/network/mock.ts index 38e82a4f19dca..b421f7af56603 100644 --- a/x-pack/plugins/security_solution/server/lib/network/mock.ts +++ b/x-pack/plugins/security_solution/server/lib/network/mock.ts @@ -21,7 +21,7 @@ export const mockOptions: NetworkTopNFlowRequestOptions = { timestamp: '@timestamp', }, }, - timerange: { interval: '12h', to: 1549852006071, from: 1549765606071 }, + timerange: { interval: '12h', to: '2019-02-11T02:26:46.071Z', from: '2019-02-11T02:26:46.071Z' }, pagination: { activePage: 0, cursorStart: 0, diff --git a/x-pack/plugins/security_solution/server/lib/network/query_dns.dsl.ts b/x-pack/plugins/security_solution/server/lib/network/query_dns.dsl.ts index 96b5d260b1544..e7c86e1d3d66b 100644 --- a/x-pack/plugins/security_solution/server/lib/network/query_dns.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/network/query_dns.dsl.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEmpty } from 'lodash/fp'; + import { Direction, NetworkDnsFields, NetworkDnsSortField } from '../../graphql/types'; import { assertUnreachable, createQueryFilterClauses } from '../../utils/build_query'; @@ -57,6 +59,7 @@ const createIncludePTRFilter = (isPtrIncluded: boolean) => export const buildDnsQuery = ({ defaultIndex, + docValueFields, filterQuery, isPtrIncluded, networkDnsSortField, @@ -74,6 +77,7 @@ export const buildDnsQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, @@ -84,6 +88,7 @@ export const buildDnsQuery = ({ index: defaultIndex, ignoreUnavailable: true, body: { + ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), aggregations: { ...getCountAgg(), dns_name_query_count: { diff --git a/x-pack/plugins/security_solution/server/lib/network/query_http.dsl.ts b/x-pack/plugins/security_solution/server/lib/network/query_http.dsl.ts index 3e33b5af80a85..a2d1963414be1 100644 --- a/x-pack/plugins/security_solution/server/lib/network/query_http.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/network/query_http.dsl.ts @@ -29,7 +29,11 @@ export const buildHttpQuery = ({ }: NetworkHttpRequestOptions) => { const filter = [ ...createQueryFilterClauses(filterQuery), - { range: { [timestamp]: { gte: from, lte: to } } }, + { + range: { + [timestamp]: { gte: from, lte: to, format: 'strict_date_optional_time' }, + }, + }, { exists: { field: 'http.request.method' } }, ]; diff --git a/x-pack/plugins/security_solution/server/lib/network/query_top_countries.dsl.ts b/x-pack/plugins/security_solution/server/lib/network/query_top_countries.dsl.ts index 40bee7eee8155..93ffc35161fa9 100644 --- a/x-pack/plugins/security_solution/server/lib/network/query_top_countries.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/network/query_top_countries.dsl.ts @@ -36,7 +36,11 @@ export const buildTopCountriesQuery = ({ }: NetworkTopCountriesRequestOptions) => { const filter = [ ...createQueryFilterClauses(filterQuery), - { range: { [timestamp]: { gte: from, lte: to } } }, + { + range: { + [timestamp]: { gte: from, lte: to, format: 'strict_date_optional_time' }, + }, + }, ]; const dslQuery = { diff --git a/x-pack/plugins/security_solution/server/lib/network/query_top_n_flow.dsl.ts b/x-pack/plugins/security_solution/server/lib/network/query_top_n_flow.dsl.ts index 47bbabf5505ca..7cb8b76e7b524 100644 --- a/x-pack/plugins/security_solution/server/lib/network/query_top_n_flow.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/network/query_top_n_flow.dsl.ts @@ -36,7 +36,11 @@ export const buildTopNFlowQuery = ({ }: NetworkTopNFlowRequestOptions) => { const filter = [ ...createQueryFilterClauses(filterQuery), - { range: { [timestamp]: { gte: from, lte: to } } }, + { + range: { + [timestamp]: { gte: from, lte: to, format: 'strict_date_optional_time' }, + }, + }, ]; const dslQuery = { diff --git a/x-pack/plugins/security_solution/server/lib/overview/mock.ts b/x-pack/plugins/security_solution/server/lib/overview/mock.ts index 51d8a258569a8..2621c795ecd6b 100644 --- a/x-pack/plugins/security_solution/server/lib/overview/mock.ts +++ b/x-pack/plugins/security_solution/server/lib/overview/mock.ts @@ -19,7 +19,7 @@ export const mockOptionsNetwork: RequestBasicOptions = { timestamp: '@timestamp', }, }, - timerange: { interval: '12h', to: 1549852006071, from: 1549765606071 }, + timerange: { interval: '12h', to: '2019-02-11T02:26:46.071Z', from: '2019-02-10T02:26:46.071Z' }, filterQuery: {}, }; @@ -28,7 +28,11 @@ export const mockRequestNetwork = { operationName: 'GetOverviewNetworkQuery', variables: { sourceId: 'default', - timerange: { interval: '12h', from: 1549765830772, to: 1549852230772 }, + timerange: { + interval: '12h', + from: '2019-02-10T02:30:30.772Z', + to: '2019-02-11T02:30:30.772Z', + }, filterQuery: '', }, query: @@ -90,7 +94,7 @@ export const mockOptionsHost: RequestBasicOptions = { timestamp: '@timestamp', }, }, - timerange: { interval: '12h', to: 1549852006071, from: 1549765606071 }, + timerange: { interval: '12h', to: '2019-02-11T02:26:46.071Z', from: '2019-02-10T02:26:46.071Z' }, filterQuery: {}, }; @@ -99,7 +103,11 @@ export const mockRequestHost = { operationName: 'GetOverviewHostQuery', variables: { sourceId: 'default', - timerange: { interval: '12h', from: 1549765830772, to: 1549852230772 }, + timerange: { + interval: '12h', + from: '2019-02-10T02:30:30.772Z', + to: '2019-02-11T02:30:30.772Z', + }, filterQuery: '', }, query: diff --git a/x-pack/plugins/security_solution/server/lib/overview/query.dsl.ts b/x-pack/plugins/security_solution/server/lib/overview/query.dsl.ts index 30656c011ee21..8ac8233a86b82 100644 --- a/x-pack/plugins/security_solution/server/lib/overview/query.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/overview/query.dsl.ts @@ -21,6 +21,7 @@ export const buildOverviewNetworkQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, @@ -120,6 +121,7 @@ export const buildOverviewHostQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts index 2afe3197d6d64..0b10018de5bba 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts @@ -21,7 +21,7 @@ export const mockParsedObjects = [ kqlMode: 'filter', kqlQuery: { filterQuery: [Object] }, title: 'My duplicate timeline', - dateRange: { start: 1584523907294, end: 1584610307294 }, + dateRange: { start: '2020-03-18T09:31:47.294Z', end: '2020-03-19T09:31:47.294Z' }, savedQueryId: null, sort: { columnId: '@timestamp', sortDirection: 'desc' }, created: 1584828930463, @@ -80,7 +80,7 @@ export const mockUniqueParsedObjects = [ kqlMode: 'filter', kqlQuery: { filterQuery: [] }, title: 'My duplicate timeline', - dateRange: { start: 1584523907294, end: 1584610307294 }, + dateRange: { start: '2020-03-18T09:31:47.294Z', end: '2020-03-19T09:31:47.294Z' }, savedQueryId: null, sort: { columnId: '@timestamp', sortDirection: 'desc' }, created: 1584828930463, @@ -139,7 +139,7 @@ export const mockGetTimelineValue = { kqlQuery: { filterQuery: [] }, title: 'My duplicate timeline', timelineType: TimelineType.default, - dateRange: { start: 1584523907294, end: 1584610307294 }, + dateRange: { start: '2020-03-18T09:31:47.294Z', end: '2020-03-19T09:31:47.294Z' }, savedQueryId: null, sort: { columnId: '@timestamp', sortDirection: 'desc' }, created: 1584828930463, @@ -176,7 +176,7 @@ export const mockGetDraftTimelineValue = { kqlMode: 'filter', kqlQuery: { filterQuery: [] }, title: 'My duplicate timeline', - dateRange: { start: 1584523907294, end: 1584610307294 }, + dateRange: { start: '2020-03-18T09:31:47.294Z', end: '2020-03-19T09:31:47.294Z' }, savedQueryId: null, sort: { columnId: '@timestamp', sortDirection: 'desc' }, created: 1584828930463, @@ -236,7 +236,7 @@ export const mockCreatedTimeline = { kqlMode: 'filter', kqlQuery: { filterQuery: [] }, title: 'My duplicate timeline', - dateRange: { start: 1584523907294, end: 1584610307294 }, + dateRange: { start: '2020-03-18T09:31:47.294Z', end: '2020-03-19T09:31:47.294Z' }, savedQueryId: null, sort: { columnId: '@timestamp', sortDirection: 'desc' }, created: 1584828930463, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts index a314d5fb36c6d..e3aeff280678f 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts @@ -65,7 +65,7 @@ export const inputTimeline: SavedTimeline = { timelineType: TimelineType.default, templateTimelineId: null, templateTimelineVersion: 1, - dateRange: { start: 1585227005527, end: 1585313405527 }, + dateRange: { start: '2020-03-26T12:50:05.527Z', end: '2020-03-27T12:50:05.527Z' }, savedQueryId: null, sort: { columnId: '@timestamp', sortDirection: 'desc' }, }; @@ -281,7 +281,7 @@ export const mockTimelines = () => ({ }, }, title: 'test no.2', - dateRange: { start: 1582538951145, end: 1582625351145 }, + dateRange: { start: '2020-02-24T10:09:11.145Z', end: '2020-02-25T10:09:11.145Z' }, savedQueryId: null, sort: { columnId: '@timestamp', sortDirection: 'desc' }, created: 1582625382448, @@ -363,7 +363,7 @@ export const mockTimelines = () => ({ }, }, title: 'test no.3', - dateRange: { start: 1582538951145, end: 1582625351145 }, + dateRange: { start: '2020-02-24T10:09:11.145Z', end: '2020-02-25T10:09:11.145Z' }, savedQueryId: null, sort: { columnId: '@timestamp', sortDirection: 'desc' }, created: 1582642817439, diff --git a/x-pack/plugins/security_solution/server/lib/tls/mock.ts b/x-pack/plugins/security_solution/server/lib/tls/mock.ts index b97a6fa509ef2..62d5e1e61570a 100644 --- a/x-pack/plugins/security_solution/server/lib/tls/mock.ts +++ b/x-pack/plugins/security_solution/server/lib/tls/mock.ts @@ -458,7 +458,7 @@ export const mockOptions = { timestamp: '@timestamp', }, }, - timerange: { interval: '12h', to: 1570801871626, from: 1570715471626 }, + timerange: { interval: '12h', to: '2019-10-11T13:51:11.626Z', from: '2019-10-10T13:51:11.626Z' }, pagination: { activePage: 0, cursorStart: 0, fakePossibleCount: 50, querySize: 10 }, filterQuery: {}, fields: [ diff --git a/x-pack/plugins/security_solution/server/lib/tls/query_tls.dsl.ts b/x-pack/plugins/security_solution/server/lib/tls/query_tls.dsl.ts index bc65be642dabc..82f16ff58d135 100644 --- a/x-pack/plugins/security_solution/server/lib/tls/query_tls.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/tls/query_tls.dsl.ts @@ -62,7 +62,11 @@ export const buildTlsQuery = ({ }: TlsRequestOptions) => { const defaultFilter = [ ...createQueryFilterClauses(filterQuery), - { range: { [timestamp]: { gte: from, lte: to } } }, + { + range: { + [timestamp]: { gte: from, lte: to, format: 'strict_date_optional_time' }, + }, + }, ]; const filter = ip ? [...defaultFilter, { term: { [`${flowTarget}.ip`]: ip } }] : defaultFilter; diff --git a/x-pack/plugins/security_solution/server/lib/uncommon_processes/query.dsl.ts b/x-pack/plugins/security_solution/server/lib/uncommon_processes/query.dsl.ts index 24cae53d5d353..4563c769cdc31 100644 --- a/x-pack/plugins/security_solution/server/lib/uncommon_processes/query.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/uncommon_processes/query.dsl.ts @@ -28,6 +28,7 @@ export const buildQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, diff --git a/x-pack/plugins/security_solution/server/utils/build_query/calculate_timeseries_interval.ts b/x-pack/plugins/security_solution/server/utils/build_query/calculate_timeseries_interval.ts index 78aadf75e54c3..ded37db677d6d 100644 --- a/x-pack/plugins/security_solution/server/utils/build_query/calculate_timeseries_interval.ts +++ b/x-pack/plugins/security_solution/server/utils/build_query/calculate_timeseries_interval.ts @@ -89,6 +89,6 @@ export const calculateAuto = { }), }; -export const calculateTimeSeriesInterval = (from: number, to: number) => { - return `${Math.floor((to - from) / 32)}ms`; +export const calculateTimeSeriesInterval = (from: string, to: string) => { + return `${Math.floor(moment(to).diff(moment(from)) / 32)}ms`; }; diff --git a/x-pack/plugins/security_solution/server/utils/build_query/create_options.test.ts b/x-pack/plugins/security_solution/server/utils/build_query/create_options.test.ts index 5ca67ad6ae51f..e83ca7418ad3d 100644 --- a/x-pack/plugins/security_solution/server/utils/build_query/create_options.test.ts +++ b/x-pack/plugins/security_solution/server/utils/build_query/create_options.test.ts @@ -34,9 +34,19 @@ describe('createOptions', () => { pagination: { limit: 5, }, + docValueFields: [ + { + field: '@timestamp', + format: 'date_time', + }, + { + field: 'event.end', + format: 'date_time', + }, + ], timerange: { - from: 10, - to: 0, + from: '2020-07-08T08:00:00.000Z', + to: '2020-07-08T20:00:00.000Z', interval: '12 hours ago', }, sortField: { sortFieldId: 'sort-1', direction: Direction.asc }, @@ -73,10 +83,20 @@ describe('createOptions', () => { limit: 5, }, filterQuery: {}, + docValueFields: [ + { + field: '@timestamp', + format: 'date_time', + }, + { + field: 'event.end', + format: 'date_time', + }, + ], fields: [], timerange: { - from: 10, - to: 0, + from: '2020-07-08T08:00:00.000Z', + to: '2020-07-08T20:00:00.000Z', interval: '12 hours ago', }, }; @@ -102,10 +122,51 @@ describe('createOptions', () => { limit: 5, }, filterQuery: {}, + docValueFields: [ + { + field: '@timestamp', + format: 'date_time', + }, + { + field: 'event.end', + format: 'date_time', + }, + ], + fields: [], + timerange: { + from: '2020-07-08T08:00:00.000Z', + to: '2020-07-08T20:00:00.000Z', + interval: '12 hours ago', + }, + }; + expect(options).toEqual(expected); + }); + + test('should create options given all input except docValueFields', () => { + const argsWithoutSort: Args = omit('docValueFields', args); + const options = createOptions(source, argsWithoutSort, info); + const expected: RequestOptions = { + defaultIndex: DEFAULT_INDEX_PATTERN, + sourceConfiguration: { + fields: { + host: 'host-1', + container: 'container-1', + message: ['message-1'], + pod: 'pod-1', + tiebreaker: 'tiebreaker', + timestamp: 'timestamp-1', + }, + }, + sortField: { sortFieldId: 'sort-1', direction: Direction.asc }, + pagination: { + limit: 5, + }, + filterQuery: {}, + docValueFields: [], fields: [], timerange: { - from: 10, - to: 0, + from: '2020-07-08T08:00:00.000Z', + to: '2020-07-08T20:00:00.000Z', interval: '12 hours ago', }, }; diff --git a/x-pack/plugins/security_solution/server/utils/build_query/create_options.ts b/x-pack/plugins/security_solution/server/utils/build_query/create_options.ts index 5a5aff2a2d54e..5895c0a404136 100644 --- a/x-pack/plugins/security_solution/server/utils/build_query/create_options.ts +++ b/x-pack/plugins/security_solution/server/utils/build_query/create_options.ts @@ -13,6 +13,7 @@ import { SortField, Source, TimerangeInput, + DocValueFieldsInput, } from '../../graphql/types'; import { RequestOptions, RequestOptionsPaginated } from '../../lib/framework'; import { parseFilterQuery } from '../serialized_query'; @@ -32,6 +33,7 @@ export interface Args { filterQuery?: string | null; sortField?: SortField | null; defaultIndex: string[]; + docValueFields?: DocValueFieldsInput[]; } export interface ArgsPaginated { timerange?: TimerangeInput | null; @@ -39,6 +41,7 @@ export interface ArgsPaginated { filterQuery?: string | null; sortField?: SortField | null; defaultIndex: string[]; + docValueFields?: DocValueFieldsInput[]; } export const createOptions = ( @@ -50,6 +53,7 @@ export const createOptions = ( const fields = getFields(getOr([], 'fieldNodes[0]', info)); return { defaultIndex: args.defaultIndex, + docValueFields: args.docValueFields ?? [], sourceConfiguration: source.configuration, timerange: args.timerange!, pagination: args.pagination!, @@ -70,6 +74,7 @@ export const createOptionsPaginated = ( const fields = getFields(getOr([], 'fieldNodes[0]', info)); return { defaultIndex: args.defaultIndex, + docValueFields: args.docValueFields ?? [], sourceConfiguration: source.configuration, timerange: args.timerange!, pagination: args.pagination!, diff --git a/x-pack/test/api_integration/apis/security_solution/authentications.ts b/x-pack/test/api_integration/apis/security_solution/authentications.ts index 90784ec786d48..277ac7316e92d 100644 --- a/x-pack/test/api_integration/apis/security_solution/authentications.ts +++ b/x-pack/test/api_integration/apis/security_solution/authentications.ts @@ -10,8 +10,8 @@ import { authenticationsQuery } from '../../../../plugins/security_solution/publ import { GetAuthenticationsQuery } from '../../../../plugins/security_solution/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; -const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); -const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); +const FROM = '2000-01-01T00:00:00.000Z'; +const TO = '3000-01-01T00:00:00.000Z'; // typical values that have to change after an update from "scripts/es_archiver" const HOST_NAME = 'zeek-newyork-sha-aa8df15'; @@ -44,6 +44,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 1, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -73,6 +74,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 2, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) diff --git a/x-pack/test/api_integration/apis/security_solution/hosts.ts b/x-pack/test/api_integration/apis/security_solution/hosts.ts index 9ee85f7ff03dc..2904935719d2c 100644 --- a/x-pack/test/api_integration/apis/security_solution/hosts.ts +++ b/x-pack/test/api_integration/apis/security_solution/hosts.ts @@ -18,8 +18,8 @@ import { HostFirstLastSeenGqlQuery } from '../../../../plugins/security_solution import { HostsTableQuery } from '../../../../plugins/security_solution/public/hosts/containers/hosts/hosts_table.gql_query'; import { FtrProviderContext } from '../../ftr_provider_context'; -const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); -const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); +const FROM = '2000-01-01T00:00:00.000Z'; +const TO = '3000-01-01T00:00:00.000Z'; // typical values that have to change after an update from "scripts/es_archiver" const HOST_NAME = 'Ubuntu'; @@ -47,6 +47,7 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], sort: { field: HostsFields.lastSeen, direction: Direction.asc, @@ -84,6 +85,7 @@ export default function ({ getService }: FtrProviderContext) { direction: Direction.asc, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], pagination: { activePage: 2, cursorStart: 1, @@ -150,6 +152,7 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -167,6 +170,7 @@ export default function ({ getService }: FtrProviderContext) { sourceId: 'default', hostName: 'zeek-sensor-san-francisco', defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], }, }) .then((resp) => { diff --git a/x-pack/test/api_integration/apis/security_solution/ip_overview.ts b/x-pack/test/api_integration/apis/security_solution/ip_overview.ts index 1dc0f6390ce7e..6493c07617991 100644 --- a/x-pack/test/api_integration/apis/security_solution/ip_overview.ts +++ b/x-pack/test/api_integration/apis/security_solution/ip_overview.ts @@ -25,6 +25,7 @@ export default function ({ getService }: FtrProviderContext) { sourceId: 'default', ip: '151.205.0.17', defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -52,6 +53,7 @@ export default function ({ getService }: FtrProviderContext) { sourceId: 'default', ip: '185.53.91.88', defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) diff --git a/x-pack/test/api_integration/apis/security_solution/kpi_host_details.ts b/x-pack/test/api_integration/apis/security_solution/kpi_host_details.ts index 4b296078ff443..c446fbb149e3a 100644 --- a/x-pack/test/api_integration/apis/security_solution/kpi_host_details.ts +++ b/x-pack/test/api_integration/apis/security_solution/kpi_host_details.ts @@ -17,8 +17,8 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('filebeat/default')); after(() => esArchiver.unload('filebeat/default')); - const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); - const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); + const FROM = '2000-01-01T00:00:00.000Z'; + const TO = '3000-01-01T00:00:00.000Z'; const expectedResult = { __typename: 'KpiHostDetailsData', authSuccess: 0, @@ -86,6 +86,7 @@ export default function ({ getService }: FtrProviderContext) { }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], hostName: 'zeek-sensor-san-francisco', + docValueFields: [], inspect: false, }, }) @@ -167,6 +168,7 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], hostName: 'zeek-sensor-san-francisco', inspect: false, }, diff --git a/x-pack/test/api_integration/apis/security_solution/kpi_hosts.ts b/x-pack/test/api_integration/apis/security_solution/kpi_hosts.ts index 30a0eac386c9d..dcea52edcddf9 100644 --- a/x-pack/test/api_integration/apis/security_solution/kpi_hosts.ts +++ b/x-pack/test/api_integration/apis/security_solution/kpi_hosts.ts @@ -17,8 +17,8 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('filebeat/default')); after(() => esArchiver.unload('filebeat/default')); - const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); - const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); + const FROM = '2000-01-01T00:00:00.000Z'; + const TO = '3000-01-01T00:00:00.000Z'; const expectedResult = { __typename: 'KpiHostsData', hosts: 1, @@ -108,6 +108,7 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -122,8 +123,8 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('auditbeat/default')); after(() => esArchiver.unload('auditbeat/default')); - const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); - const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); + const FROM = '2000-01-01T00:00:00.000Z'; + const TO = '3000-01-01T00:00:00.000Z'; const expectedResult = { __typename: 'KpiHostsData', hosts: 1, @@ -212,6 +213,7 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) diff --git a/x-pack/test/api_integration/apis/security_solution/kpi_network.ts b/x-pack/test/api_integration/apis/security_solution/kpi_network.ts index 6d6eee7d3468d..654607913d44a 100644 --- a/x-pack/test/api_integration/apis/security_solution/kpi_network.ts +++ b/x-pack/test/api_integration/apis/security_solution/kpi_network.ts @@ -17,8 +17,8 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('filebeat/default')); after(() => esArchiver.unload('filebeat/default')); - const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); - const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); + const FROM = '2000-01-01T00:00:00.000Z'; + const TO = '3000-01-01T00:00:00.000Z'; const expectedResult = { __typename: 'KpiNetworkData', networkEvents: 6158, @@ -85,6 +85,7 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -99,8 +100,8 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('packetbeat/default')); after(() => esArchiver.unload('packetbeat/default')); - const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); - const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); + const FROM = '2000-01-01T00:00:00.000Z'; + const TO = '3000-01-01T00:00:00.000Z'; const expectedResult = { __typename: 'KpiNetworkData', networkEvents: 6158, @@ -166,6 +167,7 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) diff --git a/x-pack/test/api_integration/apis/security_solution/network_dns.ts b/x-pack/test/api_integration/apis/security_solution/network_dns.ts index 9d88c7bc2389b..e5f3ed18d32ea 100644 --- a/x-pack/test/api_integration/apis/security_solution/network_dns.ts +++ b/x-pack/test/api_integration/apis/security_solution/network_dns.ts @@ -21,8 +21,8 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('packetbeat/dns')); after(() => esArchiver.unload('packetbeat/dns')); - const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); - const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); + const FROM = '2000-01-01T00:00:00.000Z'; + const TO = '3000-01-01T00:00:00.000Z'; it('Make sure that we get Dns data and sorting by uniqueDomains ascending', () => { return client @@ -30,6 +30,7 @@ export default function ({ getService }: FtrProviderContext) { query: networkDnsQuery, variables: { defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, isPtrIncluded: false, pagination: { @@ -65,6 +66,7 @@ export default function ({ getService }: FtrProviderContext) { query: networkDnsQuery, variables: { defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], isDnsHistogram: false, inspect: false, isPtrIncluded: false, diff --git a/x-pack/test/api_integration/apis/security_solution/network_top_n_flow.ts b/x-pack/test/api_integration/apis/security_solution/network_top_n_flow.ts index bbe934d840deb..6033fdfefa4db 100644 --- a/x-pack/test/api_integration/apis/security_solution/network_top_n_flow.ts +++ b/x-pack/test/api_integration/apis/security_solution/network_top_n_flow.ts @@ -24,8 +24,8 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('filebeat/default')); after(() => esArchiver.unload('filebeat/default')); - const FROM = new Date('2019-02-09T01:57:24.870Z').valueOf(); - const TO = new Date('2019-02-12T01:57:24.870Z').valueOf(); + const FROM = '2019-02-09T01:57:24.870Z'; + const TO = '2019-02-12T01:57:24.870Z'; it('Make sure that we get Source NetworkTopNFlow data with bytes_in descending sort', () => { return client @@ -47,6 +47,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 10, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -84,6 +85,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 10, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -121,6 +123,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 10, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -155,6 +158,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 20, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) diff --git a/x-pack/test/api_integration/apis/security_solution/overview_host.ts b/x-pack/test/api_integration/apis/security_solution/overview_host.ts index 1224fe3bd7ddd..ffbf9d89fc112 100644 --- a/x-pack/test/api_integration/apis/security_solution/overview_host.ts +++ b/x-pack/test/api_integration/apis/security_solution/overview_host.ts @@ -19,8 +19,8 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('auditbeat/overview')); after(() => esArchiver.unload('auditbeat/overview')); - const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); - const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); + const FROM = '2000-01-01T00:00:00.000Z'; + const TO = '3000-01-01T00:00:00.000Z'; const expectedResult = { auditbeatAuditd: 2194, auditbeatFIM: 4, @@ -53,6 +53,7 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: DEFAULT_INDEX_PATTERN, + docValueFields: [], inspect: false, }, }) diff --git a/x-pack/test/api_integration/apis/security_solution/overview_network.ts b/x-pack/test/api_integration/apis/security_solution/overview_network.ts index b7f4184f2eeca..6976b225a4d2a 100644 --- a/x-pack/test/api_integration/apis/security_solution/overview_network.ts +++ b/x-pack/test/api_integration/apis/security_solution/overview_network.ts @@ -17,8 +17,8 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('filebeat/default')); after(() => esArchiver.unload('filebeat/default')); - const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); - const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); + const FROM = '2000-01-01T00:00:00.000Z'; + const TO = '3000-01-01T00:00:00.000Z'; const expectedResult = { auditbeatSocket: 0, @@ -45,6 +45,7 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -59,8 +60,8 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('packetbeat/overview')); after(() => esArchiver.unload('packetbeat/overview')); - const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); - const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); + const FROM = '2000-01-01T00:00:00.000Z'; + const TO = '3000-01-01T00:00:00.000Z'; const expectedResult = { auditbeatSocket: 0, filebeatCisco: 0, @@ -86,6 +87,7 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -100,8 +102,8 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('auditbeat/overview')); after(() => esArchiver.unload('auditbeat/overview')); - const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); - const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); + const FROM = '2000-01-01T00:00:00.000Z'; + const TO = '3000-01-01T00:00:00.000Z'; const expectedResult = { auditbeatSocket: 0, filebeatCisco: 0, @@ -127,6 +129,7 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) diff --git a/x-pack/test/api_integration/apis/security_solution/saved_objects/timeline.ts b/x-pack/test/api_integration/apis/security_solution/saved_objects/timeline.ts index 12e2378037c0a..10ba9621c0430 100644 --- a/x-pack/test/api_integration/apis/security_solution/saved_objects/timeline.ts +++ b/x-pack/test/api_integration/apis/security_solution/saved_objects/timeline.ts @@ -137,7 +137,7 @@ export default function ({ getService }: FtrProviderContext) { }, }, title: 'some title', - dateRange: { start: 1560195800755, end: 1560282200756 }, + dateRange: { start: '2019-06-10T19:43:20.755Z', end: '2019-06-11T19:43:20.756Z' }, sort: { columnId: '@timestamp', sortDirection: 'desc' }, }; const response = await client.mutate({ diff --git a/x-pack/test/api_integration/apis/security_solution/sources.ts b/x-pack/test/api_integration/apis/security_solution/sources.ts index 7b4df5e23ca26..a9bbf09a9e6f9 100644 --- a/x-pack/test/api_integration/apis/security_solution/sources.ts +++ b/x-pack/test/api_integration/apis/security_solution/sources.ts @@ -25,6 +25,7 @@ export default function ({ getService }: FtrProviderContext) { variables: { sourceId: 'default', defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], }, }) .then((resp) => { diff --git a/x-pack/test/api_integration/apis/security_solution/timeline.ts b/x-pack/test/api_integration/apis/security_solution/timeline.ts index 9d4084a0e41b0..5bd015a130a5a 100644 --- a/x-pack/test/api_integration/apis/security_solution/timeline.ts +++ b/x-pack/test/api_integration/apis/security_solution/timeline.ts @@ -13,8 +13,8 @@ import { } from '../../../../plugins/security_solution/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; -const LTE = new Date('3000-01-01T00:00:00.000Z').valueOf(); -const GTE = new Date('2000-01-01T00:00:00.000Z').valueOf(); +const TO = '3000-01-01T00:00:00.000Z'; +const FROM = '2000-01-01T00:00:00.000Z'; // typical values that have to change after an update from "scripts/es_archiver" const DATA_COUNT = 2; @@ -37,13 +37,13 @@ const FILTER_VALUE = { filter: [ { bool: { - should: [{ range: { '@timestamp': { gte: GTE } } }], + should: [{ range: { '@timestamp': { gte: FROM } } }], minimum_should_match: 1, }, }, { bool: { - should: [{ range: { '@timestamp': { lte: LTE } } }], + should: [{ range: { '@timestamp': { lte: TO } } }], minimum_should_match: 1, }, }, @@ -80,7 +80,13 @@ export default function ({ getService }: FtrProviderContext) { }, fieldRequested: ['@timestamp', 'host.name'], defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, + timerange: { + from: FROM, + to: TO, + interval: '12h', + }, }, }) .then((resp) => { @@ -110,7 +116,13 @@ export default function ({ getService }: FtrProviderContext) { }, fieldRequested: ['@timestamp', 'host.name'], defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, + timerange: { + from: FROM, + to: TO, + interval: '12h', + }, }, }) .then((resp) => { diff --git a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts index 3524d7bf2db07..35f419fde894d 100644 --- a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts +++ b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts @@ -314,6 +314,7 @@ export default function ({ getService }: FtrProviderContext) { indexName: INDEX_NAME, eventId: ID, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], }, }) .then((resp) => { diff --git a/x-pack/test/api_integration/apis/security_solution/tls.ts b/x-pack/test/api_integration/apis/security_solution/tls.ts index cbddcf6b0f935..e5f6233d50d59 100644 --- a/x-pack/test/api_integration/apis/security_solution/tls.ts +++ b/x-pack/test/api_integration/apis/security_solution/tls.ts @@ -14,8 +14,8 @@ import { } from '../../../../plugins/security_solution/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; -const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); -const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); +const FROM = '2000-01-01T00:00:00.000Z'; +const TO = '3000-01-01T00:00:00.000Z'; const SOURCE_IP = '10.128.0.35'; const DESTINATION_IP = '74.125.129.95'; @@ -117,6 +117,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 10, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -149,6 +150,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 10, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -186,6 +188,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 10, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -217,6 +220,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 10, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) diff --git a/x-pack/test/api_integration/apis/security_solution/uncommon_processes.ts b/x-pack/test/api_integration/apis/security_solution/uncommon_processes.ts index a08ba8d8a7cd1..f1e064bcc37bb 100644 --- a/x-pack/test/api_integration/apis/security_solution/uncommon_processes.ts +++ b/x-pack/test/api_integration/apis/security_solution/uncommon_processes.ts @@ -10,8 +10,8 @@ import { uncommonProcessesQuery } from '../../../../plugins/security_solution/pu import { GetUncommonProcessesQuery } from '../../../../plugins/security_solution/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; -const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); -const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); +const FROM = '2000-01-01T00:00:00.000Z'; +const TO = '3000-01-01T00:00:00.000Z'; // typical values that have to change after an update from "scripts/es_archiver" const TOTAL_COUNT = 3; @@ -45,6 +45,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 1, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }); @@ -72,6 +73,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 2, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }); @@ -99,6 +101,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 1, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }); @@ -126,6 +129,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 1, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }); diff --git a/x-pack/test/api_integration/apis/security_solution/users.ts b/x-pack/test/api_integration/apis/security_solution/users.ts index eb7fba88a6a46..abb2c5b2f5bbd 100644 --- a/x-pack/test/api_integration/apis/security_solution/users.ts +++ b/x-pack/test/api_integration/apis/security_solution/users.ts @@ -14,8 +14,8 @@ import { } from '../../../../plugins/security_solution/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; -const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); -const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); +const FROM = '2000-01-01T00:00:00.000Z'; +const TO = '3000-01-01T00:00:00.000Z'; const IP = '0.0.0.0'; export default function ({ getService }: FtrProviderContext) { @@ -38,6 +38,7 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], ip: IP, flowTarget: FlowTarget.destination, sort: { field: UsersFields.name, direction: Direction.asc }, From 8da80fe82781bdf86f8e3c369dd66bab75102a71 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Tue, 14 Jul 2020 15:39:26 -0600 Subject: [PATCH 47/82] [Security] Adds field mapping support to rule creation Part II (#71402) ## Summary Followup to https://github.com/elastic/kibana/pull/70288, which includes: - [X] Rule Execution logic for: - [X] Severity Override - [X] Risk Score Override - [X] Rule Name Override - [X] Timestamp Override - [X] Support for toggling display of Building Block Rules: - [X] Main Detections Page - [X] Rule Details Page - [X] Integrates `AutocompleteField` for: - [X] Severity Override - [X] Risk Score Override - [X] Rule Name Override - [X] Timestamp Override - [X] Fixes rehydration of `EditAboutStep` in `Edit Rule` - [X] Fixes `Rule Details` Description rollup Additional followup cleanup: - [ ] Adds risk_score` to `risk_score_mapping` - [ ] Improves field validation - [ ] Disables override fields for ML Rules - [ ] Orders `SeverityMapping` by `severity` on create/update - [ ] Allow unbounded max-signals ### 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/master/packages/kbn-i18n/README.md) - [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials - Syncing w/ @benskelker - [X] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios ### For maintainers - [X] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) --- .../schemas/common/schemas.ts | 1 + .../common/components/autocomplete/field.tsx | 16 ++- .../autocomplete/field_value_match.tsx | 3 + .../common/components/utility_bar/index.ts | 1 + .../common/components/utility_bar/styles.tsx | 33 ++++- .../utility_bar/utility_bar_group.tsx | 8 +- .../utility_bar/utility_bar_section.tsx | 8 +- .../utility_bar/utility_bar_spacer.tsx | 19 +++ .../alerts_utility_bar/index.test.tsx | 2 + .../alerts_table/alerts_utility_bar/index.tsx | 42 ++++++- .../alerts_utility_bar/translations.ts | 14 +++ .../alerts_table/default_config.tsx | 19 +++ .../components/alerts_table/index.test.tsx | 2 + .../components/alerts_table/index.tsx | 8 ++ .../components/alerts_table/translations.ts | 6 +- .../rules/autocomplete_field/index.tsx | 75 +++++++++++ .../rules/description_step/helpers.test.tsx | 17 ++- .../rules/description_step/helpers.tsx | 75 ++++++++++- .../rules/description_step/index.test.tsx | 4 +- .../rules/description_step/index.tsx | 15 +-- .../rules/risk_score_mapping/index.tsx | 103 ++++++++++----- .../rules/risk_score_mapping/translations.tsx | 7 ++ .../rules/severity_mapping/index.tsx | 119 ++++++++++++++---- .../rules/severity_mapping/translations.tsx | 7 ++ .../rules/step_about_rule/index.tsx | 69 +++++----- .../rules/step_about_rule/translations.ts | 6 + .../detection_engine/detection_engine.tsx | 27 +++- .../rules/create/helpers.test.ts | 8 -- .../detection_engine/rules/create/helpers.ts | 4 +- .../detection_engine/rules/details/index.tsx | 29 ++++- .../detection_engine/rules/edit/index.tsx | 3 +- .../pages/detection_engine/rules/types.ts | 4 +- .../signals/build_bulk_body.ts | 1 + .../signals/build_events_query.test.ts | 6 + .../signals/build_events_query.ts | 11 +- .../signals/build_rule.test.ts | 5 +- .../detection_engine/signals/build_rule.ts | 34 ++++- .../signals/find_threshold_signals.ts | 1 + .../build_risk_score_from_mapping.test.ts | 26 ++++ .../mappings/build_risk_score_from_mapping.ts | 42 +++++++ .../build_rule_name_from_mapping.test.ts | 26 ++++ .../mappings/build_rule_name_from_mapping.ts | 40 ++++++ .../build_severity_from_mapping.test.ts | 26 ++++ .../mappings/build_severity_from_mapping.ts | 50 ++++++++ .../signals/search_after_bulk_create.ts | 1 + .../signals/single_search_after.test.ts | 3 + .../signals/single_search_after.ts | 4 + 47 files changed, 874 insertions(+), 156 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_spacer.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index 542cbe8916032..273ea72a2ffe3 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -255,6 +255,7 @@ export const severity_mapping_item = t.exact( severity, }) ); +export type SeverityMappingItem = t.TypeOf; export const severity_mapping = t.array(severity_mapping_item); export type SeverityMapping = t.TypeOf; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx index 8a6f049c96037..ed844b5130c77 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx @@ -17,6 +17,7 @@ interface OperatorProps { isLoading: boolean; isDisabled: boolean; isClearable: boolean; + fieldTypeFilter?: string[]; fieldInputWidth?: number; onChange: (a: IFieldType[]) => void; } @@ -28,13 +29,22 @@ export const FieldComponent: React.FC = ({ isLoading = false, isDisabled = false, isClearable = false, + fieldTypeFilter = [], fieldInputWidth = 190, onChange, }): JSX.Element => { const getLabel = useCallback((field): string => field.name, []); - const optionsMemo = useMemo((): IFieldType[] => (indexPattern ? indexPattern.fields : []), [ - indexPattern, - ]); + const optionsMemo = useMemo((): IFieldType[] => { + if (indexPattern != null) { + if (fieldTypeFilter.length > 0) { + return indexPattern.fields.filter((f) => fieldTypeFilter.includes(f.type)); + } else { + return indexPattern.fields; + } + } else { + return []; + } + }, [fieldTypeFilter, indexPattern]); const selectedOptionsMemo = useMemo((): IFieldType[] => (selectedField ? [selectedField] : []), [ selectedField, ]); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx index 4d96d6638132b..32a82af114bae 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx @@ -22,6 +22,7 @@ interface AutocompleteFieldMatchProps { isLoading: boolean; isDisabled: boolean; isClearable: boolean; + fieldInputWidth?: number; onChange: (arg: string) => void; } @@ -33,6 +34,7 @@ export const AutocompleteFieldMatchComponent: React.FC { const [isLoadingSuggestions, suggestions, updateSuggestions] = useFieldValueAutocomplete({ @@ -97,6 +99,7 @@ export const AutocompleteFieldMatchComponent: React.FC diff --git a/x-pack/plugins/security_solution/public/common/components/utility_bar/index.ts b/x-pack/plugins/security_solution/public/common/components/utility_bar/index.ts index b07fe8bb847c7..44e19a951b6ac 100644 --- a/x-pack/plugins/security_solution/public/common/components/utility_bar/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/utility_bar/index.ts @@ -8,4 +8,5 @@ export { UtilityBar } from './utility_bar'; export { UtilityBarAction } from './utility_bar_action'; export { UtilityBarGroup } from './utility_bar_group'; export { UtilityBarSection } from './utility_bar_section'; +export { UtilityBarSpacer } from './utility_bar_spacer'; export { UtilityBarText } from './utility_bar_text'; diff --git a/x-pack/plugins/security_solution/public/common/components/utility_bar/styles.tsx b/x-pack/plugins/security_solution/public/common/components/utility_bar/styles.tsx index e1554da491a8b..dd6b66350052e 100644 --- a/x-pack/plugins/security_solution/public/common/components/utility_bar/styles.tsx +++ b/x-pack/plugins/security_solution/public/common/components/utility_bar/styles.tsx @@ -14,6 +14,14 @@ export interface BarProps { border?: boolean; } +export interface BarSectionProps { + grow?: boolean; +} + +export interface BarGroupProps { + grow?: boolean; +} + export const Bar = styled.aside.attrs({ className: 'siemUtilityBar', })` @@ -36,8 +44,8 @@ Bar.displayName = 'Bar'; export const BarSection = styled.div.attrs({ className: 'siemUtilityBar__section', -})` - ${({ theme }) => css` +})` + ${({ grow, theme }) => css` & + & { margin-top: ${theme.eui.euiSizeS}; } @@ -53,14 +61,18 @@ export const BarSection = styled.div.attrs({ margin-left: ${theme.eui.euiSize}; } } + ${grow && + css` + flex: 1; + `} `} `; BarSection.displayName = 'BarSection'; export const BarGroup = styled.div.attrs({ className: 'siemUtilityBar__group', -})` - ${({ theme }) => css` +})` + ${({ grow, theme }) => css` align-items: flex-start; display: flex; flex-wrap: wrap; @@ -93,6 +105,10 @@ export const BarGroup = styled.div.attrs({ margin-right: 0; } } + ${grow && + css` + flex: 1; + `} `} `; BarGroup.displayName = 'BarGroup'; @@ -118,3 +134,12 @@ export const BarAction = styled.div.attrs({ `} `; BarAction.displayName = 'BarAction'; + +export const BarSpacer = styled.div.attrs({ + className: 'siemUtilityBar__spacer', +})` + ${() => css` + flex: 1; + `} +`; +BarSpacer.displayName = 'BarSpacer'; diff --git a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_group.tsx b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_group.tsx index 723035df672a9..d67be4882ceec 100644 --- a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_group.tsx +++ b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_group.tsx @@ -6,14 +6,14 @@ import React from 'react'; -import { BarGroup } from './styles'; +import { BarGroup, BarGroupProps } from './styles'; -export interface UtilityBarGroupProps { +export interface UtilityBarGroupProps extends BarGroupProps { children: React.ReactNode; } -export const UtilityBarGroup = React.memo(({ children }) => ( - {children} +export const UtilityBarGroup = React.memo(({ grow, children }) => ( + {children} )); UtilityBarGroup.displayName = 'UtilityBarGroup'; diff --git a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_section.tsx b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_section.tsx index 42532c0355607..d88ec35f977c3 100644 --- a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_section.tsx +++ b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_section.tsx @@ -6,14 +6,14 @@ import React from 'react'; -import { BarSection } from './styles'; +import { BarSection, BarSectionProps } from './styles'; -export interface UtilityBarSectionProps { +export interface UtilityBarSectionProps extends BarSectionProps { children: React.ReactNode; } -export const UtilityBarSection = React.memo(({ children }) => ( - {children} +export const UtilityBarSection = React.memo(({ grow, children }) => ( + {children} )); UtilityBarSection.displayName = 'UtilityBarSection'; diff --git a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_spacer.tsx b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_spacer.tsx new file mode 100644 index 0000000000000..f57b300266f7b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_spacer.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { BarSpacer } from './styles'; + +export interface UtilityBarSpacerProps { + dataTestSubj?: string; +} + +export const UtilityBarSpacer = React.memo(({ dataTestSubj }) => ( + +)); + +UtilityBarSpacer.displayName = 'UtilityBarSpacer'; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx index 7c884d773209a..cbbe43cc03568 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx @@ -24,6 +24,8 @@ describe('AlertsUtilityBar', () => { currentFilter="closed" selectAll={jest.fn()} showClearSelection={true} + showBuildingBlockAlerts={false} + onShowBuildingBlockAlertsChanged={jest.fn()} updateAlertsStatus={jest.fn()} /> ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx index 6533be1a9b09c..bedc23790541c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx @@ -8,8 +8,9 @@ import { isEmpty } from 'lodash/fp'; import React, { useCallback } from 'react'; import numeral from '@elastic/numeral'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiCheckbox } from '@elastic/eui'; import styled from 'styled-components'; + import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; import { Link } from '../../../../common/components/link_icon'; import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; @@ -18,6 +19,7 @@ import { UtilityBarAction, UtilityBarGroup, UtilityBarSection, + UtilityBarSpacer, UtilityBarText, } from '../../../../common/components/utility_bar'; import * as i18n from './translations'; @@ -34,6 +36,8 @@ interface AlertsUtilityBarProps { currentFilter: Status; selectAll: () => void; selectedEventIds: Readonly>; + showBuildingBlockAlerts: boolean; + onShowBuildingBlockAlertsChanged: (showBuildingBlockAlerts: boolean) => void; showClearSelection: boolean; totalCount: number; updateAlertsStatus: UpdateAlertsStatus; @@ -52,6 +56,8 @@ const AlertsUtilityBarComponent: React.FC = ({ selectedEventIds, currentFilter, selectAll, + showBuildingBlockAlerts, + onShowBuildingBlockAlertsChanged, showClearSelection, updateAlertsStatus, }) => { @@ -125,17 +131,36 @@ const AlertsUtilityBarComponent: React.FC = ({ ); + const UtilityBarAdditionalFiltersContent = (closePopover: () => void) => ( + + + ) => { + closePopover(); + onShowBuildingBlockAlertsChanged(e.target.checked); + }} + checked={showBuildingBlockAlerts} + color="text" + data-test-subj="showBuildingBlockAlertsCheckbox" + label={i18n.ADDITIONAL_FILTERS_ACTIONS_SHOW_BUILDING_BLOCK} + /> + + + ); + return ( <> - + {i18n.SHOWING_ALERTS(formattedTotalCount, totalCount)} - + {canUserCRUD && hasIndexWrite && ( <> @@ -174,6 +199,17 @@ const AlertsUtilityBarComponent: React.FC = ({ )} + + + {i18n.ADDITIONAL_FILTERS_ACTIONS} + diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts index 51e1b6f6e4c46..eb4ca405b084e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts @@ -27,6 +27,20 @@ export const SELECT_ALL_ALERTS = (totalAlertsFormatted: string, totalAlerts: num 'Select all {totalAlertsFormatted} {totalAlerts, plural, =1 {alert} other {alerts}}', }); +export const ADDITIONAL_FILTERS_ACTIONS = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersTitle', + { + defaultMessage: 'Additional filters', + } +); + +export const ADDITIONAL_FILTERS_ACTIONS_SHOW_BUILDING_BLOCK = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersActions.showBuildingBlockTitle', + { + defaultMessage: 'Include building block alerts', + } +); + export const CLEAR_SELECTION = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.utilityBar.clearSelectionTitle', { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index 6f1f2e46dce3d..71cf5c10de764 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -81,6 +81,25 @@ export const buildAlertsRuleIdFilter = (ruleId: string): Filter[] => [ }, ]; +export const buildShowBuildingBlockFilter = (showBuildingBlockAlerts: boolean): Filter[] => [ + ...(showBuildingBlockAlerts + ? [] + : [ + { + meta: { + alias: null, + negate: true, + disabled: false, + type: 'exists', + key: 'signal.rule.building_block_type', + value: 'exists', + }, + // @ts-ignore TODO: Rework parent typings to support ExistsFilter[] + exists: { field: 'signal.rule.building_block_type' }, + }, + ]), +]; + export const alertsHeaders: ColumnHeaderOptions[] = [ { columnHeaderType: defaultColumnHeaderType, 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 index 563f2ea60cded..cc3a47017a835 100644 --- 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 @@ -37,6 +37,8 @@ describe('AlertsTableComponent', () => { clearEventsLoading={jest.fn()} setEventsDeleted={jest.fn()} clearEventsDeleted={jest.fn()} + showBuildingBlockAlerts={false} + onShowBuildingBlockAlertsChanged={jest.fn()} updateTimelineIsLoading={jest.fn()} updateTimeline={jest.fn()} /> diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 391598ebda03d..87c631b80e38b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -64,6 +64,8 @@ interface OwnProps { hasIndexWrite: boolean; from: string; loading: boolean; + showBuildingBlockAlerts: boolean; + onShowBuildingBlockAlertsChanged: (showBuildingBlockAlerts: boolean) => void; signalsIndex: string; to: string; } @@ -94,6 +96,8 @@ export const AlertsTableComponent: React.FC = ({ selectedEventIds, setEventsDeleted, setEventsLoading, + showBuildingBlockAlerts, + onShowBuildingBlockAlertsChanged, signalsIndex, to, updateTimeline, @@ -302,6 +306,8 @@ export const AlertsTableComponent: React.FC = ({ currentFilter={filterGroup} selectAll={selectAllCallback} selectedEventIds={selectedEventIds} + showBuildingBlockAlerts={showBuildingBlockAlerts} + onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChanged} showClearSelection={showClearSelectionAction} totalCount={totalCount} updateAlertsStatus={updateAlertsStatusCallback.bind(null, refetchQuery)} @@ -313,6 +319,8 @@ export const AlertsTableComponent: React.FC = ({ hasIndexWrite, clearSelectionCallback, filterGroup, + showBuildingBlockAlerts, + onShowBuildingBlockAlertsChanged, loadingEventIds.length, selectAllCallback, selectedEventIds, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts index 0f55469bbfda2..e5e8635b9e799 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts @@ -20,21 +20,21 @@ export const ALERTS_DOCUMENT_TYPE = i18n.translate( export const OPEN_ALERTS = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.openAlertsTitle', { - defaultMessage: 'Open alerts', + defaultMessage: 'Open', } ); export const CLOSED_ALERTS = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.closedAlertsTitle', { - defaultMessage: 'Closed alerts', + defaultMessage: 'Closed', } ); export const IN_PROGRESS_ALERTS = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.inProgressAlertsTitle', { - defaultMessage: 'In progress alerts', + defaultMessage: 'In progress', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx new file mode 100644 index 0000000000000..0346511874104 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useMemo } from 'react'; +import { EuiFormRow } from '@elastic/eui'; +import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +import { FieldComponent } from '../../../../common/components/autocomplete/field'; +import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; + +interface AutocompleteFieldProps { + dataTestSubj: string; + field: FieldHook; + idAria: string; + indices: IIndexPattern; + isDisabled: boolean; + fieldType: string; + placeholder?: string; +} + +export const AutocompleteField = ({ + dataTestSubj, + field, + idAria, + indices, + isDisabled, + fieldType, + placeholder, +}: AutocompleteFieldProps) => { + const handleFieldChange = useCallback( + ([newField]: IFieldType[]): void => { + // TODO: Update onChange type in FieldComponent as newField can be undefined + field.setValue(newField?.name ?? ''); + }, + [field] + ); + + const selectedField = useMemo(() => { + const existingField = (field.value as string) ?? ''; + const [newSelectedField] = indices.fields.filter( + ({ name }) => existingField != null && existingField === name + ); + return newSelectedField; + }, [field.value, indices]); + + const fieldTypeFilter = useMemo(() => [fieldType], [fieldType]); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx index 41ee91845a8ec..2a6cd3fc5bb7a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import { EuiLoadingSpinner } from '@elastic/eui'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; @@ -328,10 +328,19 @@ describe('helpers', () => { describe('buildSeverityDescription', () => { test('returns ListItem with passed in label and SeverityBadge component', () => { - const result: ListItems[] = buildSeverityDescription('Test label', 'Test description value'); + const result: ListItems[] = buildSeverityDescription({ + value: 'low', + mapping: [{ field: 'host.name', operator: 'equals', value: 'hello', severity: 'high' }], + }); - expect(result[0].title).toEqual('Test label'); - expect(result[0].description).toEqual(); + expect(result[0].title).toEqual('Severity'); + expect(result[0].description).toEqual(); + expect(result[1].title).toEqual('Severity override'); + + const wrapper = mount(result[1].description as React.ReactElement); + expect(wrapper.find('[data-test-subj="severityOverrideSeverity0"]').first().text()).toEqual( + 'High' + ); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx index 8393f2230dcfe..1110c8c098988 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx @@ -13,12 +13,16 @@ import { EuiSpacer, EuiLink, EuiText, + EuiIcon, + EuiToolTip, } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; +import * as i18nSeverity from '../severity_mapping/translations'; +import * as i18nRiskScore from '../risk_score_mapping/translations'; import { Threshold } from '../../../../../common/detection_engine/schemas/common/schemas'; import { RuleType } from '../../../../../common/detection_engine/types'; import { esFilters } from '../../../../../../../../src/plugins/data/public'; @@ -30,6 +34,7 @@ import { BuildQueryBarDescription, BuildThreatDescription, ListItems } from './t import { SeverityBadge } from '../severity_badge'; import ListTreeIcon from './assets/list_tree_icon.svg'; import { assertUnreachable } from '../../../../common/lib/helpers'; +import { AboutStepRiskScore, AboutStepSeverity } from '../../../pages/detection_engine/rules/types'; const NoteDescriptionContainer = styled(EuiFlexItem)` height: 105px; @@ -219,11 +224,75 @@ export const buildStringArrayDescription = ( return []; }; -export const buildSeverityDescription = (label: string, value: string): ListItems[] => [ +const OverrideColumn = styled(EuiFlexItem)` + width: 125px; + max-width: 125px; + overflow: hidden; + text-overflow: ellipsis; +`; + +export const buildSeverityDescription = (severity: AboutStepSeverity): ListItems[] => [ { - title: label, - description: , + title: i18nSeverity.DEFAULT_SEVERITY, + description: , + }, + ...severity.mapping.map((severityItem, index) => { + return { + title: index === 0 ? i18nSeverity.SEVERITY_MAPPING : '', + description: ( + + + + <>{severityItem.field} + + + + <>{severityItem.value} + + + + + + + + + ), + }; + }), +]; + +export const buildRiskScoreDescription = (riskScore: AboutStepRiskScore): ListItems[] => [ + { + title: i18nRiskScore.RISK_SCORE, + description: riskScore.value, }, + ...riskScore.mapping.map((riskScoreItem, index) => { + return { + title: index === 0 ? i18nRiskScore.RISK_SCORE_MAPPING : '', + description: ( + + + + <>{riskScoreItem.field} + + + + + + {'signal.rule.risk_score'} + + ), + }; + }), ]; const MyRefUrlLink = styled(EuiLink)` diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx index 5a2a44a284e3b..4a2d17ec126fb 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx @@ -450,7 +450,7 @@ describe('description_step', () => { mockFilterManager ); - expect(result[0].title).toEqual('Severity label'); + expect(result[0].title).toEqual('Severity'); expect(React.isValidElement(result[0].description)).toBeTruthy(); }); }); @@ -464,7 +464,7 @@ describe('description_step', () => { mockFilterManager ); - expect(result[0].title).toEqual('Risk score label'); + expect(result[0].title).toEqual('Risk score'); expect(result[0].description).toEqual(21); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index 51624d04cb58b..0b341050fa9d5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -34,6 +34,7 @@ import { buildUnorderedListArrayDescription, buildUrlsDescription, buildNoteDescription, + buildRiskScoreDescription, buildRuleTypeDescription, buildThresholdDescription, } from './helpers'; @@ -192,18 +193,12 @@ export const getDescriptionItem = ( } else if (Array.isArray(get(field, data))) { const values: string[] = get(field, data); return buildStringArrayDescription(label, field, values); - // TODO: Add custom UI for Risk/Severity Mappings (and fix missing label) } else if (field === 'riskScore') { - const val: AboutStepRiskScore = get(field, data); - return [ - { - title: label, - description: val.value, - }, - ]; + const values: AboutStepRiskScore = get(field, data); + return buildRiskScoreDescription(values); } else if (field === 'severity') { - const val: AboutStepSeverity = get(field, data); - return buildSeverityDescription(label, val.value); + const values: AboutStepSeverity = get(field, data); + return buildSeverityDescription(values); } else if (field === 'timeline') { const timeline = get(field, data) as FieldValueTimeline; return [ diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx index bdf1ac600faef..c9e2cb1a8ca24 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx @@ -6,7 +6,6 @@ import { EuiFormRow, - EuiFieldText, EuiCheckbox, EuiText, EuiFlexGroup, @@ -15,12 +14,15 @@ import { EuiIcon, EuiSpacer, } from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import * as i18n from './translations'; import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { CommonUseField } from '../../../../cases/components/create'; import { AboutStepRiskScore } from '../../../pages/detection_engine/rules/types'; +import { FieldComponent } from '../../../../common/components/autocomplete/field'; +import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; const NestedContent = styled.div` margin-left: 24px; @@ -38,20 +40,47 @@ interface RiskScoreFieldProps { dataTestSubj: string; field: FieldHook; idAria: string; - indices: string[]; + indices: IIndexPattern; + placeholder?: string; } -export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskScoreFieldProps) => { - const [isRiskScoreMappingSelected, setIsRiskScoreMappingSelected] = useState(false); +export const RiskScoreField = ({ + dataTestSubj, + field, + idAria, + indices, + placeholder, +}: RiskScoreFieldProps) => { + const [isRiskScoreMappingChecked, setIsRiskScoreMappingChecked] = useState(false); + const [initialFieldCheck, setInitialFieldCheck] = useState(true); - const updateRiskScoreMapping = useCallback( - (event) => { + const fieldTypeFilter = useMemo(() => ['number'], []); + + useEffect(() => { + if ( + !isRiskScoreMappingChecked && + initialFieldCheck && + (field.value as AboutStepRiskScore).mapping?.length > 0 + ) { + setIsRiskScoreMappingChecked(true); + setInitialFieldCheck(false); + } + }, [ + field, + initialFieldCheck, + isRiskScoreMappingChecked, + setIsRiskScoreMappingChecked, + setInitialFieldCheck, + ]); + + const handleFieldChange = useCallback( + ([newField]: IFieldType[]): void => { const values = field.value as AboutStepRiskScore; field.setValue({ value: values.value, mapping: [ { - field: event.target.value, + field: newField?.name ?? '', operator: 'equals', value: '', }, @@ -61,11 +90,23 @@ export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskSco [field] ); - const severityLabel = useMemo(() => { + const selectedField = useMemo(() => { + const existingField = (field.value as AboutStepRiskScore).mapping?.[0]?.field ?? ''; + const [newSelectedField] = indices.fields.filter( + ({ name }) => existingField != null && existingField === name + ); + return newSelectedField; + }, [field.value, indices]); + + const handleRiskScoreMappingChecked = useCallback(() => { + setIsRiskScoreMappingChecked(!isRiskScoreMappingChecked); + }, [isRiskScoreMappingChecked, setIsRiskScoreMappingChecked]); + + const riskScoreLabel = useMemo(() => { return (
- {i18n.RISK_SCORE} + {i18n.DEFAULT_RISK_SCORE} {i18n.RISK_SCORE_DESCRIPTION} @@ -73,19 +114,15 @@ export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskSco ); }, []); - const severityMappingLabel = useMemo(() => { + const riskScoreMappingLabel = useMemo(() => { return (
- setIsRiskScoreMappingSelected(!isRiskScoreMappingSelected)} - > + setIsRiskScoreMappingSelected(e.target.checked)} + checked={isRiskScoreMappingChecked} + onChange={handleRiskScoreMappingChecked} /> {i18n.RISK_SCORE_MAPPING} @@ -96,13 +133,13 @@ export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskSco
); - }, [isRiskScoreMappingSelected, setIsRiskScoreMappingSelected]); + }, [handleRiskScoreMappingChecked, isRiskScoreMappingChecked]); return ( {i18n.RISK_SCORE_MAPPING_DETAILS} ) : ( '' @@ -147,7 +184,7 @@ export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskSco > - {isRiskScoreMappingSelected && ( + {isRiskScoreMappingChecked && ( @@ -156,7 +193,7 @@ export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskSco - {i18n.RISK_SCORE} + {i18n.DEFAULT_RISK_SCORE} @@ -164,12 +201,18 @@ export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskSco - diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/translations.tsx index a75bf19b5b3c4..24e82a8f95a6b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/translations.tsx @@ -8,6 +8,13 @@ import { i18n } from '@kbn/i18n'; export const RISK_SCORE = i18n.translate( 'xpack.securitySolution.alerts.riskScoreMapping.riskScoreTitle', + { + defaultMessage: 'Risk score', + } +); + +export const DEFAULT_RISK_SCORE = i18n.translate( + 'xpack.securitySolution.alerts.riskScoreMapping.defaultRiskScoreTitle', { defaultMessage: 'Default risk score', } diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx index 47c45a6bdf88d..579c60579b32e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx @@ -6,7 +6,6 @@ import { EuiFormRow, - EuiFieldText, EuiCheckbox, EuiText, EuiFlexGroup, @@ -15,14 +14,23 @@ import { EuiIcon, EuiSpacer, } from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import * as i18n from './translations'; import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { SeverityOptionItem } from '../step_about_rule/data'; import { CommonUseField } from '../../../../cases/components/create'; import { AboutStepSeverity } from '../../../pages/detection_engine/rules/types'; +import { + IFieldType, + IIndexPattern, +} from '../../../../../../../../src/plugins/data/common/index_patterns'; +import { FieldComponent } from '../../../../common/components/autocomplete/field'; +import { AutocompleteFieldMatchComponent } from '../../../../common/components/autocomplete/field_value_match'; +const SeverityMappingParentContainer = styled(EuiFlexItem)` + max-width: 471px; +`; const NestedContent = styled.div` margin-left: 24px; `; @@ -39,7 +47,7 @@ interface SeverityFieldProps { dataTestSubj: string; field: FieldHook; idAria: string; - indices: string[]; + indices: IIndexPattern; options: SeverityOptionItem[]; } @@ -47,13 +55,32 @@ export const SeverityField = ({ dataTestSubj, field, idAria, - indices, // TODO: To be used with autocomplete fields once https://github.com/elastic/kibana/pull/67013 is merged + indices, options, }: SeverityFieldProps) => { const [isSeverityMappingChecked, setIsSeverityMappingChecked] = useState(false); + const [initialFieldCheck, setInitialFieldCheck] = useState(true); + const fieldValueInputWidth = 160; - const updateSeverityMapping = useCallback( - (index: number, severity: string, mappingField: string, event) => { + useEffect(() => { + if ( + !isSeverityMappingChecked && + initialFieldCheck && + (field.value as AboutStepSeverity).mapping?.length > 0 + ) { + setIsSeverityMappingChecked(true); + setInitialFieldCheck(false); + } + }, [ + field, + initialFieldCheck, + isSeverityMappingChecked, + setIsSeverityMappingChecked, + setInitialFieldCheck, + ]); + + const handleFieldChange = useCallback( + (index: number, severity: string, [newField]: IFieldType[]): void => { const values = field.value as AboutStepSeverity; field.setValue({ value: values.value, @@ -61,7 +88,7 @@ export const SeverityField = ({ ...values.mapping.slice(0, index), { ...values.mapping[index], - [mappingField]: event.target.value, + field: newField?.name ?? '', operator: 'equals', severity, }, @@ -72,6 +99,41 @@ export const SeverityField = ({ [field] ); + const handleFieldMatchValueChange = useCallback( + (index: number, severity: string, newMatchValue: string): void => { + const values = field.value as AboutStepSeverity; + field.setValue({ + value: values.value, + mapping: [ + ...values.mapping.slice(0, index), + { + ...values.mapping[index], + value: newMatchValue, + operator: 'equals', + severity, + }, + ...values.mapping.slice(index + 1), + ], + }); + }, + [field] + ); + + const selectedState = useMemo(() => { + return ( + (field.value as AboutStepSeverity).mapping?.map((mapping) => { + const [newSelectedField] = indices.fields.filter( + ({ name }) => mapping.field != null && mapping.field === name + ); + return { field: newSelectedField, value: mapping.value }; + }) ?? [] + ); + }, [field.value, indices]); + + const handleSeverityMappingSelected = useCallback(() => { + setIsSeverityMappingChecked(!isSeverityMappingChecked); + }, [isSeverityMappingChecked, setIsSeverityMappingChecked]); + const severityLabel = useMemo(() => { return (
@@ -87,16 +149,12 @@ export const SeverityField = ({ const severityMappingLabel = useMemo(() => { return (
- setIsSeverityMappingChecked(!isSeverityMappingChecked)} - > + setIsSeverityMappingChecked(e.target.checked)} + onChange={handleSeverityMappingSelected} /> {i18n.SEVERITY_MAPPING} @@ -107,7 +165,7 @@ export const SeverityField = ({
); - }, [isSeverityMappingChecked, setIsSeverityMappingChecked]); + }, [handleSeverityMappingSelected, isSeverityMappingChecked]); return ( @@ -137,7 +195,7 @@ export const SeverityField = ({ - + - {i18n.SEVERITY} + {i18n.DEFAULT_SEVERITY} @@ -177,22 +235,33 @@ export const SeverityField = ({ - - @@ -208,7 +277,7 @@ export const SeverityField = ({ )} - + ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx index 9c9784bac6b63..f0bfc5f4637ab 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx @@ -13,6 +13,13 @@ export const SEVERITY = i18n.translate( } ); +export const DEFAULT_SEVERITY = i18n.translate( + 'xpack.securitySolution.alerts.severityMapping.defaultSeverityTitle', + { + defaultMessage: 'Severity', + } +); + export const SOURCE_FIELD = i18n.translate( 'xpack.securitySolution.alerts.severityMapping.sourceFieldTitle', { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx index 7f7ee94ed85b7..3616643874a0a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx @@ -38,6 +38,8 @@ import { MarkdownEditorForm } from '../../../../common/components/markdown_edito import { setFieldValue } from '../../../pages/detection_engine/rules/helpers'; import { SeverityField } from '../severity_mapping'; import { RiskScoreField } from '../risk_score_mapping'; +import { useFetchIndexPatterns } from '../../../containers/detection_engine/rules'; +import { AutocompleteField } from '../autocomplete_field'; const CommonUseField = getUseField({ component: Field }); @@ -90,6 +92,9 @@ const StepAboutRuleComponent: FC = ({ setStepData, }) => { const [myStepData, setMyStepData] = useState(stepAboutDefaultValue); + const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns( + defineRuleData?.index ?? [] + ); const { form } = useForm({ defaultValue: myStepData, @@ -149,7 +154,6 @@ const StepAboutRuleComponent: FC = ({ }} /> - = ({ componentProps={{ 'data-test-subj': 'detectionEngineStepAboutRuleSeverityField', idAria: 'detectionEngineStepAboutRuleSeverityField', - isDisabled: isLoading, + isDisabled: isLoading || indexPatternLoading, options: severityOptions, - indices: defineRuleData?.index ?? [], + indices: indexPatterns, }} /> @@ -184,7 +188,8 @@ const StepAboutRuleComponent: FC = ({ componentProps={{ 'data-test-subj': 'detectionEngineStepAboutRuleRiskScore', idAria: 'detectionEngineStepAboutRuleRiskScore', - isDisabled: isLoading, + isDisabled: isLoading || indexPatternLoading, + indices: indexPatterns, }} /> @@ -196,7 +201,7 @@ const StepAboutRuleComponent: FC = ({ 'data-test-subj': 'detectionEngineStepAboutRuleTags', euiFieldProps: { fullWidth: true, - isDisabled: isLoading, + isDisabled: isLoading || indexPatternLoading, placeholder: '', }, }} @@ -277,7 +282,7 @@ const StepAboutRuleComponent: FC = ({ }} /> - + = ({ /> - - - + - - - + {({ severity }) => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts index c179128c56d92..3a5aa3c56c3df 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts @@ -26,6 +26,12 @@ export const ADD_FALSE_POSITIVE = i18n.translate( defaultMessage: 'Add false positive example', } ); +export const BUILDING_BLOCK = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.buildingBlockLabel', + { + defaultMessage: 'Building block', + } +); export const LOW = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionLowDescription', 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 cdff8ea4ab928..aef9f2adcbcc8 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 @@ -5,7 +5,7 @@ */ import { EuiSpacer } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { StickyContainer } from 'react-sticky'; import { connect, ConnectedProps } from 'react-redux'; @@ -39,6 +39,7 @@ import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unau import * as i18n from './translations'; import { LinkButton } from '../../../common/components/links'; import { useFormatUrl } from '../../../common/components/link_to'; +import { buildShowBuildingBlockFilter } from '../../components/alerts_table/default_config'; export const DetectionEnginePageComponent: React.FC = ({ filters, @@ -62,6 +63,7 @@ export const DetectionEnginePageComponent: React.FC = ({ const history = useHistory(); const [lastAlerts] = useAlertInfo({}); const { formatUrl } = useFormatUrl(SecurityPageName.detections); + const [showBuildingBlockAlerts, setShowBuildingBlockAlerts] = useState(false); const loading = userInfoLoading || listsConfigLoading; const updateDateRangeCallback = useCallback( @@ -87,6 +89,24 @@ export const DetectionEnginePageComponent: React.FC = ({ [history] ); + const alertsHistogramDefaultFilters = useMemo( + () => [...filters, ...buildShowBuildingBlockFilter(showBuildingBlockAlerts)], + [filters, showBuildingBlockAlerts] + ); + + // AlertsTable manages global filters itself, so not including `filters` + const alertsTableDefaultFilters = useMemo( + () => buildShowBuildingBlockFilter(showBuildingBlockAlerts), + [showBuildingBlockAlerts] + ); + + const onShowBuildingBlockAlertsChangedCallback = useCallback( + (newShowBuildingBlockAlerts: boolean) => { + setShowBuildingBlockAlerts(newShowBuildingBlockAlerts); + }, + [setShowBuildingBlockAlerts] + ); + const indexToAdd = useMemo(() => (signalIndexName == null ? [] : [signalIndexName]), [ signalIndexName, ]); @@ -145,7 +165,7 @@ export const DetectionEnginePageComponent: React.FC = ({ = ({ hasIndexWrite={hasIndexWrite ?? false} canUserCRUD={(canUserCRUD ?? false) && (hasEncryptionKey ?? false)} from={from} + defaultFilters={alertsTableDefaultFilters} + showBuildingBlockAlerts={showBuildingBlockAlerts} + onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChangedCallback} signalsIndex={signalIndexName ?? ''} to={to} /> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts index f402303c4c621..745518b90df00 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts @@ -348,7 +348,6 @@ describe('helpers', () => { references: ['www.test.co'], risk_score: 21, risk_score_mapping: [], - rule_name_override: '', severity: 'low', severity_mapping: [], tags: ['tag1', 'tag2'], @@ -369,7 +368,6 @@ describe('helpers', () => { ], }, ], - timestamp_override: '', }; expect(result).toEqual(expected); @@ -392,7 +390,6 @@ describe('helpers', () => { references: ['www.test.co'], risk_score: 21, risk_score_mapping: [], - rule_name_override: '', severity: 'low', severity_mapping: [], tags: ['tag1', 'tag2'], @@ -413,7 +410,6 @@ describe('helpers', () => { ], }, ], - timestamp_override: '', }; expect(result).toEqual(expected); @@ -434,7 +430,6 @@ describe('helpers', () => { references: ['www.test.co'], risk_score: 21, risk_score_mapping: [], - rule_name_override: '', severity: 'low', severity_mapping: [], tags: ['tag1', 'tag2'], @@ -455,7 +450,6 @@ describe('helpers', () => { ], }, ], - timestamp_override: '', }; expect(result).toEqual(expected); @@ -508,7 +502,6 @@ describe('helpers', () => { references: ['www.test.co'], risk_score: 21, risk_score_mapping: [], - rule_name_override: '', severity: 'low', severity_mapping: [], tags: ['tag1', 'tag2'], @@ -519,7 +512,6 @@ describe('helpers', () => { technique: [{ id: '456', name: 'technique1', reference: 'technique reference' }], }, ], - timestamp_override: '', }; expect(result).toEqual(expected); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index 4bb7196e17db5..c419dd142cfbe 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -167,7 +167,7 @@ export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRule references: references.filter((item) => !isEmpty(item)), risk_score: riskScore.value, risk_score_mapping: riskScore.mapping, - rule_name_override: ruleNameOverride, + rule_name_override: ruleNameOverride !== '' ? ruleNameOverride : undefined, severity: severity.value, severity_mapping: severity.mapping, threat: threat @@ -180,7 +180,7 @@ export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRule return { id, name, reference }; }), })), - timestamp_override: timestampOverride, + timestamp_override: timestampOverride !== '' ? timestampOverride : undefined, ...(!isEmpty(note) ? { note } : {}), ...rest, }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 45a1c89cec621..2e7ef1180f4e3 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -17,7 +17,7 @@ import { EuiToolTip, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import React, { FC, memo, useCallback, useMemo, useState } from 'react'; +import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'; import { useParams, useHistory } from 'react-router-dom'; import { StickyContainer } from 'react-sticky'; import { connect, ConnectedProps } from 'react-redux'; @@ -48,7 +48,10 @@ import { OverviewEmpty } from '../../../../../overview/components/overview_empty import { useAlertInfo } from '../../../../components/alerts_info'; import { StepDefineRule } from '../../../../components/rules/step_define_rule'; import { StepScheduleRule } from '../../../../components/rules/step_schedule_rule'; -import { buildAlertsRuleIdFilter } from '../../../../components/alerts_table/default_config'; +import { + buildAlertsRuleIdFilter, + buildShowBuildingBlockFilter, +} from '../../../../components/alerts_table/default_config'; import { NoWriteAlertsCallOut } from '../../../../components/no_write_alerts_callout'; import * as detectionI18n from '../../translations'; import { ReadOnlyCallOut } from '../../../../components/rules/read_only_callout'; @@ -134,6 +137,7 @@ export const RuleDetailsPageComponent: FC = ({ scheduleRuleData: null, }; const [lastAlerts] = useAlertInfo({ ruleId }); + const [showBuildingBlockAlerts, setShowBuildingBlockAlerts] = useState(false); const mlCapabilities = useMlCapabilities(); const history = useHistory(); const { formatUrl } = useFormatUrl(SecurityPageName.detections); @@ -184,9 +188,17 @@ export const RuleDetailsPageComponent: FC = ({ [isLoading, rule] ); + // Set showBuildingBlockAlerts if rule is a Building Block Rule otherwise we won't show alerts + useEffect(() => { + setShowBuildingBlockAlerts(rule?.building_block_type != null); + }, [rule]); + const alertDefaultFilters = useMemo( - () => (ruleId != null ? buildAlertsRuleIdFilter(ruleId) : []), - [ruleId] + () => [ + ...(ruleId != null ? buildAlertsRuleIdFilter(ruleId) : []), + ...buildShowBuildingBlockFilter(showBuildingBlockAlerts), + ], + [ruleId, showBuildingBlockAlerts] ); const alertMergedFilters = useMemo(() => [...alertDefaultFilters, ...filters], [ @@ -262,6 +274,13 @@ export const RuleDetailsPageComponent: FC = ({ [history, ruleId] ); + const onShowBuildingBlockAlertsChangedCallback = useCallback( + (newShowBuildingBlockAlerts: boolean) => { + setShowBuildingBlockAlerts(newShowBuildingBlockAlerts); + }, + [setShowBuildingBlockAlerts] + ); + const { indicesExist, indexPattern } = useWithSource('default', indexToAdd); const exceptionLists = useMemo((): { @@ -447,6 +466,8 @@ export const RuleDetailsPageComponent: FC = ({ hasIndexWrite={hasIndexWrite ?? false} from={from} loading={loading} + showBuildingBlockAlerts={showBuildingBlockAlerts} + onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChangedCallback} signalsIndex={signalIndexName ?? ''} to={to} /> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx index 87cb5e77697b5..0900cdb8f4789 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx @@ -160,12 +160,13 @@ const EditRulePageComponent: FC = () => { <> - {myAboutRuleForm.data != null && ( + {myAboutRuleForm.data != null && myDefineRuleForm.data != null && ( )} 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 e7daff0947b0d..b501536e5b387 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 @@ -145,10 +145,10 @@ export interface AboutStepRuleJson { risk_score_mapping: RiskScoreMapping; references: string[]; false_positives: string[]; - rule_name_override: RuleNameOverride; + rule_name_override?: RuleNameOverride; tags: string[]; threat: IMitreEnterpriseAttack[]; - timestamp_override: TimestampOverride; + timestamp_override?: TimestampOverride; note?: string; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts index 75c4d75cedf1d..218750ac30a2a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts @@ -51,6 +51,7 @@ export const buildBulkBody = ({ enabled, createdAt, createdBy, + doc, updatedAt, updatedBy, interval, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts index 452ba958876d6..ccf8a9bec3159 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts @@ -15,6 +15,7 @@ describe('create_signals', () => { filter: {}, size: 100, searchAfterSortId: undefined, + timestampOverride: undefined, }); expect(query).toEqual({ allowNoIndices: true, @@ -85,6 +86,7 @@ describe('create_signals', () => { filter: {}, size: 100, searchAfterSortId: '', + timestampOverride: undefined, }); expect(query).toEqual({ allowNoIndices: true, @@ -156,6 +158,7 @@ describe('create_signals', () => { filter: {}, size: 100, searchAfterSortId: fakeSortId, + timestampOverride: undefined, }); expect(query).toEqual({ allowNoIndices: true, @@ -228,6 +231,7 @@ describe('create_signals', () => { filter: {}, size: 100, searchAfterSortId: fakeSortIdNumber, + timestampOverride: undefined, }); expect(query).toEqual({ allowNoIndices: true, @@ -299,6 +303,7 @@ describe('create_signals', () => { filter: {}, size: 100, searchAfterSortId: undefined, + timestampOverride: undefined, }); expect(query).toEqual({ allowNoIndices: true, @@ -377,6 +382,7 @@ describe('create_signals', () => { filter: {}, size: 100, searchAfterSortId: undefined, + timestampOverride: undefined, }); expect(query).toEqual({ allowNoIndices: true, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts index dcf3a90364a40..96db7e1eb53b7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { TimestampOverrideOrUndefined } from '../../../../common/detection_engine/schemas/common/schemas'; + interface BuildEventsSearchQuery { aggregations?: unknown; index: string[]; @@ -12,6 +14,7 @@ interface BuildEventsSearchQuery { filter: unknown; size: number; searchAfterSortId: string | number | undefined; + timestampOverride: TimestampOverrideOrUndefined; } export const buildEventsSearchQuery = ({ @@ -22,7 +25,9 @@ export const buildEventsSearchQuery = ({ filter, size, searchAfterSortId, + timestampOverride, }: BuildEventsSearchQuery) => { + const timestamp = timestampOverride ?? '@timestamp'; const filterWithTime = [ filter, { @@ -33,7 +38,7 @@ export const buildEventsSearchQuery = ({ should: [ { range: { - '@timestamp': { + [timestamp]: { gte: from, }, }, @@ -47,7 +52,7 @@ export const buildEventsSearchQuery = ({ should: [ { range: { - '@timestamp': { + [timestamp]: { lte: to, }, }, @@ -79,7 +84,7 @@ export const buildEventsSearchQuery = ({ ...(aggregations ? { aggregations } : {}), sort: [ { - '@timestamp': { + [timestamp]: { order: 'asc', }, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts index ed632ee2576dc..7257e5952ff05 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts @@ -5,7 +5,7 @@ */ import { buildRule } from './build_rule'; -import { sampleRuleAlertParams, sampleRuleGuid } from './__mocks__/es_results'; +import { sampleDocNoSortId, sampleRuleAlertParams, sampleRuleGuid } from './__mocks__/es_results'; import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; @@ -29,6 +29,7 @@ describe('buildRule', () => { ]; const rule = buildRule({ actions: [], + doc: sampleDocNoSortId(), ruleParams, name: 'some-name', id: sampleRuleGuid, @@ -97,6 +98,7 @@ describe('buildRule', () => { ruleParams.filters = undefined; const rule = buildRule({ actions: [], + doc: sampleDocNoSortId(), ruleParams, name: 'some-name', id: sampleRuleGuid, @@ -154,6 +156,7 @@ describe('buildRule', () => { ruleParams.filters = undefined; const rule = buildRule({ actions: [], + doc: sampleDocNoSortId(), ruleParams, name: 'some-name', id: sampleRuleGuid, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts index 9e118f77a73e7..e02a0154d63c9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts @@ -8,6 +8,10 @@ import { pickBy } from 'lodash/fp'; import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams } from '../types'; +import { buildRiskScoreFromMapping } from './mappings/build_risk_score_from_mapping'; +import { SignalSourceHit } from './types'; +import { buildSeverityFromMapping } from './mappings/build_severity_from_mapping'; +import { buildRuleNameFromMapping } from './mappings/build_rule_name_from_mapping'; interface BuildRuleParams { ruleParams: RuleTypeParams; @@ -17,6 +21,7 @@ interface BuildRuleParams { enabled: boolean; createdAt: string; createdBy: string; + doc: SignalSourceHit; updatedAt: string; updatedBy: string; interval: string; @@ -32,12 +37,33 @@ export const buildRule = ({ enabled, createdAt, createdBy, + doc, updatedAt, updatedBy, interval, tags, throttle, }: BuildRuleParams): Partial => { + const { riskScore, riskScoreMeta } = buildRiskScoreFromMapping({ + doc, + riskScore: ruleParams.riskScore, + riskScoreMapping: ruleParams.riskScoreMapping, + }); + + const { severity, severityMeta } = buildSeverityFromMapping({ + doc, + severity: ruleParams.severity, + severityMapping: ruleParams.severityMapping, + }); + + const { ruleName, ruleNameMeta } = buildRuleNameFromMapping({ + doc, + ruleName: name, + ruleNameMapping: ruleParams.ruleNameOverride, + }); + + const meta = { ...ruleParams.meta, ...riskScoreMeta, ...severityMeta, ...ruleNameMeta }; + return pickBy((value: unknown) => value != null, { id, rule_id: ruleParams.ruleId ?? '(unknown rule_id)', @@ -48,9 +74,9 @@ export const buildRule = ({ saved_id: ruleParams.savedId, timeline_id: ruleParams.timelineId, timeline_title: ruleParams.timelineTitle, - meta: ruleParams.meta, + meta: Object.keys(meta).length > 0 ? meta : undefined, max_signals: ruleParams.maxSignals, - risk_score: ruleParams.riskScore, // TODO: Risk Score Override via risk_score_mapping + risk_score: riskScore, risk_score_mapping: ruleParams.riskScoreMapping ?? [], output_index: ruleParams.outputIndex, description: ruleParams.description, @@ -61,11 +87,11 @@ export const buildRule = ({ interval, language: ruleParams.language, license: ruleParams.license, - name, // TODO: Rule Name Override via rule_name_override + name: ruleName, query: ruleParams.query, references: ruleParams.references, rule_name_override: ruleParams.ruleNameOverride, - severity: ruleParams.severity, // TODO: Severity Override via severity_mapping + severity, severity_mapping: ruleParams.severityMapping ?? [], tags, type: ruleParams.type, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts index a9a199f210da0..251c043adb58b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts @@ -50,6 +50,7 @@ export const findThresholdSignals = async ({ return singleSearchAfter({ aggregations, searchAfterSortId: undefined, + timestampOverride: undefined, index: inputIndexPattern, from, to, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts new file mode 100644 index 0000000000000..e1d9c7f7c8a5c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sampleDocNoSortId } from '../__mocks__/es_results'; +import { buildRiskScoreFromMapping } from './build_risk_score_from_mapping'; + +describe('buildRiskScoreFromMapping', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('risk score defaults to provided if mapping is incomplete', () => { + const riskScore = buildRiskScoreFromMapping({ + doc: sampleDocNoSortId(), + riskScore: 57, + riskScoreMapping: undefined, + }); + + expect(riskScore).toEqual({ riskScore: 57, riskScoreMeta: {} }); + }); + + // TODO: Enhance... +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts new file mode 100644 index 0000000000000..356cf95fc0d24 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { get } from 'lodash/fp'; +import { + Meta, + RiskScore, + RiskScoreMappingOrUndefined, +} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { SignalSourceHit } from '../types'; +import { RiskScore as RiskScoreIOTS } from '../../../../../common/detection_engine/schemas/types'; + +interface BuildRiskScoreFromMappingProps { + doc: SignalSourceHit; + riskScore: RiskScore; + riskScoreMapping: RiskScoreMappingOrUndefined; +} + +interface BuildRiskScoreFromMappingReturn { + riskScore: RiskScore; + riskScoreMeta: Meta; // TODO: Stricter types +} + +export const buildRiskScoreFromMapping = ({ + doc, + riskScore, + riskScoreMapping, +}: BuildRiskScoreFromMappingProps): BuildRiskScoreFromMappingReturn => { + // MVP support is for mapping from a single field + if (riskScoreMapping != null && riskScoreMapping.length > 0) { + const mappedField = riskScoreMapping[0].field; + // TODO: Expand by verifying fieldType from index via doc._index + const mappedValue = get(mappedField, doc._source); + // TODO: This doesn't seem to validate...identified riskScore > 100 😬 + if (RiskScoreIOTS.is(mappedValue)) { + return { riskScore: mappedValue, riskScoreMeta: { riskScoreOverridden: true } }; + } + } + return { riskScore, riskScoreMeta: {} }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.test.ts new file mode 100644 index 0000000000000..b509020646d1b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sampleDocNoSortId } from '../__mocks__/es_results'; +import { buildRuleNameFromMapping } from './build_rule_name_from_mapping'; + +describe('buildRuleNameFromMapping', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('rule name defaults to provided if mapping is incomplete', () => { + const ruleName = buildRuleNameFromMapping({ + doc: sampleDocNoSortId(), + ruleName: 'rule-name', + ruleNameMapping: 'message', + }); + + expect(ruleName).toEqual({ ruleName: 'rule-name', ruleNameMeta: {} }); + }); + + // TODO: Enhance... +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.ts new file mode 100644 index 0000000000000..af540ed1454ad --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as t from 'io-ts'; +import { get } from 'lodash/fp'; +import { + Meta, + Name, + RuleNameOverrideOrUndefined, +} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { SignalSourceHit } from '../types'; + +interface BuildRuleNameFromMappingProps { + doc: SignalSourceHit; + ruleName: Name; + ruleNameMapping: RuleNameOverrideOrUndefined; +} + +interface BuildRuleNameFromMappingReturn { + ruleName: Name; + ruleNameMeta: Meta; // TODO: Stricter types +} + +export const buildRuleNameFromMapping = ({ + doc, + ruleName, + ruleNameMapping, +}: BuildRuleNameFromMappingProps): BuildRuleNameFromMappingReturn => { + if (ruleNameMapping != null) { + // TODO: Expand by verifying fieldType from index via doc._index + const mappedValue = get(ruleNameMapping, doc._source); + if (t.string.is(mappedValue)) { + return { ruleName: mappedValue, ruleNameMeta: { ruleNameOverridden: true } }; + } + } + + return { ruleName, ruleNameMeta: {} }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts new file mode 100644 index 0000000000000..80950335934f4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sampleDocNoSortId } from '../__mocks__/es_results'; +import { buildSeverityFromMapping } from './build_severity_from_mapping'; + +describe('buildSeverityFromMapping', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('severity defaults to provided if mapping is incomplete', () => { + const severity = buildSeverityFromMapping({ + doc: sampleDocNoSortId(), + severity: 'low', + severityMapping: undefined, + }); + + expect(severity).toEqual({ severity: 'low', severityMeta: {} }); + }); + + // TODO: Enhance... +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts new file mode 100644 index 0000000000000..a3c4f47b491be --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { get } from 'lodash/fp'; +import { + Meta, + Severity, + SeverityMappingItem, + severity as SeverityIOTS, + SeverityMappingOrUndefined, +} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { SignalSourceHit } from '../types'; + +interface BuildSeverityFromMappingProps { + doc: SignalSourceHit; + severity: Severity; + severityMapping: SeverityMappingOrUndefined; +} + +interface BuildSeverityFromMappingReturn { + severity: Severity; + severityMeta: Meta; // TODO: Stricter types +} + +export const buildSeverityFromMapping = ({ + doc, + severity, + severityMapping, +}: BuildSeverityFromMappingProps): BuildSeverityFromMappingReturn => { + if (severityMapping != null && severityMapping.length > 0) { + let severityMatch: SeverityMappingItem | undefined; + severityMapping.forEach((mapping) => { + // TODO: Expand by verifying fieldType from index via doc._index + const mappedValue = get(mapping.field, doc._source); + if (mapping.value === mappedValue) { + severityMatch = { ...mapping }; + } + }); + + if (severityMatch != null && SeverityIOTS.is(severityMatch.severity)) { + return { + severity: severityMatch.severity, + severityMeta: { severityOverrideField: severityMatch.field }, + }; + } + } + return { severity, severityMeta: {} }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index f3025ead69a05..2a0e39cbbf237 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -144,6 +144,7 @@ export const searchAfterAndBulkCreate = async ({ logger, filter, pageSize: tuple.maxSignals < pageSize ? Math.ceil(tuple.maxSignals) : pageSize, // maximum number of docs to receive per search result. + timestampOverride: ruleParams.timestampOverride, } ); toReturn.searchAfterTimes.push(searchDuration); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts index 50b0cb27990f8..250b891eb1f2c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts @@ -31,6 +31,7 @@ describe('singleSearchAfter', () => { logger: mockLogger, pageSize: 1, filter: undefined, + timestampOverride: undefined, }); expect(searchResult).toEqual(sampleDocSearchResultsNoSortId); }); @@ -46,6 +47,7 @@ describe('singleSearchAfter', () => { logger: mockLogger, pageSize: 1, filter: undefined, + timestampOverride: undefined, }); expect(searchResult).toEqual(sampleDocSearchResultsWithSortId); }); @@ -64,6 +66,7 @@ describe('singleSearchAfter', () => { logger: mockLogger, pageSize: 1, filter: undefined, + timestampOverride: undefined, }) ).rejects.toThrow('Fake Error'); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts index daea277f14368..5667f2e47b6d7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts @@ -10,6 +10,7 @@ import { Logger } from '../../../../../../../src/core/server'; import { SignalSearchResponse } from './types'; import { buildEventsSearchQuery } from './build_events_query'; import { makeFloatString } from './utils'; +import { TimestampOverrideOrUndefined } from '../../../../common/detection_engine/schemas/common/schemas'; interface SingleSearchAfterParams { aggregations?: unknown; @@ -21,6 +22,7 @@ interface SingleSearchAfterParams { logger: Logger; pageSize: number; filter: unknown; + timestampOverride: TimestampOverrideOrUndefined; } // utilize search_after for paging results into bulk. @@ -34,6 +36,7 @@ export const singleSearchAfter = async ({ filter, logger, pageSize, + timestampOverride, }: SingleSearchAfterParams): Promise<{ searchResult: SignalSearchResponse; searchDuration: string; @@ -47,6 +50,7 @@ export const singleSearchAfter = async ({ filter, size: pageSize, searchAfterSortId, + timestampOverride, }); const start = performance.now(); const nextSearchAfterResult: SignalSearchResponse = await services.callCluster( From 06b1820df71632d5ce30d0b5c60201e6d8c72063 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Tue, 14 Jul 2020 17:50:22 -0400 Subject: [PATCH 48/82] [Monitoring] Out of the box alerting (#68805) * First draft, not quite working but a good start * More working * Support configuring throttle * Get the other alerts working too * More * Separate into individual files * Menu support as well as better integration in existing UIs * Red borders! * New overview style, and renamed alert * more visual updates * Update cpu usage and improve settings configuration in UI * Convert cluster health and license expiration alert to use legacy data model * Remove most of the custom UI and use the flyout * Add the actual alerts * Remove more code * Fix formatting * Fix up some errors * Remove unnecessary code * Updates * add more links here * Fix up linkage * Added nodes changed alert * Most of the version mismatch working * Add kibana mismatch * UI tweaks * Add timestamp * Support actions in the enable api * Move this around * Better support for changing legacy alerts * Add missing files * Update alerts * Enable alerts whenever any page is visited in SM * Tweaks * Use more practical default * Remove the buggy renderer and ensure setup mode can show all alerts * Updates * Remove unnecessary code * Remove some dead code * Cleanup * Fix snapshot * Fixes * Fixes * Fix test * Add alerts to kibana and logstash listing pages * Fix test * Add disable/mute options * Tweaks * Fix linting * Fix i18n * Adding a couple tests * Fix localization * Use http * Ensure we properly handle when an alert is resolved * Fix tests * Hide legacy alerts if not the right license * Design tweaks * Fix tests * PR feedback * Moar tests * Fix i18n * Ensure we have a control over the messaging * Fix translations * Tweaks * More localization * Copy changes * Type --- x-pack/legacy/plugins/monitoring/index.ts | 4 - x-pack/plugins/monitoring/common/constants.ts | 51 +- .../{server/alerts => common}/enums.ts | 16 +- .../plugins/monitoring/common/formatting.js | 4 +- x-pack/plugins/monitoring/common/types.ts | 48 ++ x-pack/plugins/monitoring/kibana.json | 13 +- .../monitoring/public/alerts/badge.tsx | 179 +++++++ .../monitoring/public/alerts/callout.tsx | 81 ++++ .../cpu_usage_alert/cpu_usage_alert.tsx | 28 ++ .../alerts/cpu_usage_alert/expression.tsx | 61 +++ .../cpu_usage_alert/index.ts} | 2 +- .../alerts/cpu_usage_alert/validation.tsx | 35 ++ .../alert_param_duration.tsx | 98 ++++ .../alert_param_percentage.tsx | 41 ++ .../legacy_alert}/index.ts | 2 +- .../alerts/legacy_alert/legacy_alert.tsx | 39 ++ .../public/alerts/lib/replace_tokens.tsx | 93 ++++ .../alerts/lib/should_show_alert_badge.ts | 15 + .../monitoring/public/alerts/panel.tsx | 225 +++++++++ .../monitoring/public/alerts/status.tsx | 99 ++++ .../monitoring/public/angular/app_modules.ts | 12 +- .../monitoring/public/angular/index.ts | 6 +- .../alerts/__snapshots__/status.test.tsx.snap | 65 --- .../alerts/__tests__/map_severity.js | 65 --- .../public/components/alerts/alerts.js | 191 -------- .../__snapshots__/configuration.test.tsx.snap | 121 ----- .../__snapshots__/step1.test.tsx.snap | 301 ------------ .../__snapshots__/step2.test.tsx.snap | 49 -- .../__snapshots__/step3.test.tsx.snap | 95 ---- .../configuration/configuration.test.tsx | 140 ------ .../alerts/configuration/configuration.tsx | 193 -------- .../alerts/configuration/step1.test.tsx | 331 ------------- .../components/alerts/configuration/step1.tsx | 334 ------------- .../alerts/configuration/step2.test.tsx | 51 -- .../components/alerts/configuration/step2.tsx | 38 -- .../alerts/configuration/step3.test.tsx | 48 -- .../components/alerts/configuration/step3.tsx | 47 -- .../components/alerts/formatted_alert.js | 63 --- .../components/alerts/manage_email_action.tsx | 301 ------------ .../public/components/alerts/map_severity.js | 75 --- .../public/components/alerts/status.test.tsx | 85 ---- .../public/components/alerts/status.tsx | 207 -------- .../chart/monitoring_timeseries_container.js | 79 +-- .../cluster/listing/alerts_indicator.js | 87 ---- .../components/cluster/listing/listing.js | 10 +- .../cluster/overview/alerts_panel.js | 201 -------- .../cluster/overview/elasticsearch_panel.js | 168 +++++-- .../components/cluster/overview/helpers.js | 18 +- .../components/cluster/overview/index.js | 29 +- .../cluster/overview/kibana_panel.js | 26 +- .../cluster/overview/license_text.js | 42 -- .../cluster/overview/logstash_panel.js | 30 +- .../elasticsearch/cluster_status/index.js | 3 +- .../components/elasticsearch/node/node.js | 32 +- .../elasticsearch/node_detail_status/index.js | 6 +- .../components/elasticsearch/nodes/nodes.js | 67 ++- .../components/kibana/cluster_status/index.js | 3 +- .../components/kibana/instances/instances.js | 57 +-- .../monitoring/public/components/logs/logs.js | 2 +- .../public/components/logs/logs.test.js | 4 +- .../logstash/cluster_status/index.js | 4 +- .../__snapshots__/listing.test.js.snap | 14 + .../components/logstash/listing/listing.js | 19 +- .../public/components/renderers/setup_mode.js | 2 +- .../summary_status/summary_status.js | 15 + .../plugins/monitoring/public/legacy_shims.ts | 27 +- .../monitoring/public/lib/setup_mode.tsx | 11 + x-pack/plugins/monitoring/public/plugin.ts | 53 +- .../monitoring/public/services/clusters.js | 59 ++- x-pack/plugins/monitoring/public/types.ts | 4 +- x-pack/plugins/monitoring/public/url_state.ts | 6 +- .../monitoring/public/views/alerts/index.html | 3 - .../monitoring/public/views/alerts/index.js | 126 ----- x-pack/plugins/monitoring/public/views/all.js | 1 - .../public/views/base_controller.js | 35 +- .../public/views/cluster/overview/index.js | 18 +- .../public/views/elasticsearch/node/index.js | 14 +- .../public/views/elasticsearch/nodes/index.js | 15 +- .../public/views/kibana/instance/index.js | 10 +- .../public/views/kibana/instances/index.js | 13 +- .../public/views/logstash/node/index.js | 10 +- .../public/views/logstash/nodes/index.js | 13 +- .../server/alerts/alerts_factory.test.ts | 68 +++ .../server/alerts/alerts_factory.ts | 68 +++ .../server/alerts/base_alert.test.ts | 138 ++++++ .../monitoring/server/alerts/base_alert.ts | 339 +++++++++++++ .../alerts/cluster_health_alert.test.ts | 261 ++++++++++ .../server/alerts/cluster_health_alert.ts | 273 +++++++++++ .../server/alerts/cluster_state.test.ts | 175 ------- .../monitoring/server/alerts/cluster_state.ts | 135 ------ .../server/alerts/cpu_usage_alert.test.ts | 376 +++++++++++++++ .../server/alerts/cpu_usage_alert.ts | 451 ++++++++++++++++++ ...asticsearch_version_mismatch_alert.test.ts | 251 ++++++++++ .../elasticsearch_version_mismatch_alert.ts | 263 ++++++++++ .../plugins/monitoring/server/alerts/index.ts | 15 + .../kibana_version_mismatch_alert.test.ts | 253 ++++++++++ .../alerts/kibana_version_mismatch_alert.ts | 253 ++++++++++ .../server/alerts/license_expiration.test.ts | 188 -------- .../server/alerts/license_expiration.ts | 151 ------ .../alerts/license_expiration_alert.test.ts | 281 +++++++++++ .../server/alerts/license_expiration_alert.ts | 262 ++++++++++ .../logstash_version_mismatch_alert.test.ts | 250 ++++++++++ .../alerts/logstash_version_mismatch_alert.ts | 257 ++++++++++ .../server/alerts/nodes_changed_alert.test.ts | 261 ++++++++++ .../server/alerts/nodes_changed_alert.ts | 278 +++++++++++ .../monitoring/server/alerts/types.d.ts | 105 ++-- .../lib/alerts/cluster_state.lib.test.ts | 70 --- .../server/lib/alerts/cluster_state.lib.ts | 88 ---- .../lib/alerts/fetch_cluster_state.test.ts | 39 -- .../server/lib/alerts/fetch_cluster_state.ts | 53 -- .../server/lib/alerts/fetch_clusters.ts | 7 +- .../alerts/fetch_cpu_usage_node_stats.test.ts | 228 +++++++++ .../lib/alerts/fetch_cpu_usage_node_stats.ts | 137 ++++++ .../fetch_default_email_address.test.ts | 17 - .../lib/alerts/fetch_default_email_address.ts | 13 - .../lib/alerts/fetch_legacy_alerts.test.ts | 93 ++++ .../server/lib/alerts/fetch_legacy_alerts.ts | 93 ++++ .../server/lib/alerts/fetch_licenses.test.ts | 60 --- .../server/lib/alerts/fetch_licenses.ts | 57 --- .../server/lib/alerts/fetch_status.test.ts | 167 +++++-- .../server/lib/alerts/fetch_status.ts | 92 ++-- .../lib/alerts/get_prepared_alert.test.ts | 163 ------- .../server/lib/alerts/get_prepared_alert.ts | 87 ---- .../lib/alerts/license_expiration.lib.test.ts | 64 --- .../lib/alerts/license_expiration.lib.ts | 88 ---- .../lib/alerts/map_legacy_severity.test.ts | 15 + .../server/lib/alerts/map_legacy_severity.ts | 14 + .../lib/cluster/get_clusters_from_request.js | 96 ++-- .../server/lib/errors/handle_error.js | 2 +- .../monitoring/server/license_service.ts | 2 +- x-pack/plugins/monitoring/server/plugin.ts | 116 ++--- .../server/routes/api/v1/alerts/alerts.js | 140 ------ .../server/routes/api/v1/alerts/enable.ts | 73 +++ .../server/routes/api/v1/alerts/index.js | 4 +- .../routes/api/v1/alerts/legacy_alerts.js | 57 --- .../server/routes/api/v1/alerts/status.ts | 61 +++ .../server/routes/{index.js => index.ts} | 8 +- x-pack/plugins/monitoring/server/types.ts | 93 ++++ .../translations/translations/ja-JP.json | 91 ---- .../translations/translations/zh-CN.json | 91 ---- .../triggers_actions_ui/public/index.ts | 1 + .../cluster/fixtures/multicluster.json | 11 +- .../monitoring/cluster/fixtures/overview.json | 16 - .../standalone_cluster/fixtures/cluster.json | 3 - .../standalone_cluster/fixtures/clusters.json | 6 +- .../apps/monitoring/cluster/alerts.js | 208 -------- .../apps/monitoring/cluster/overview.js | 8 - .../test/functional/apps/monitoring/index.js | 1 - .../monitoring/elasticsearch_nodes.js | 12 +- 149 files changed, 7524 insertions(+), 5861 deletions(-) rename x-pack/plugins/monitoring/{server/alerts => common}/enums.ts (54%) create mode 100644 x-pack/plugins/monitoring/common/types.ts create mode 100644 x-pack/plugins/monitoring/public/alerts/badge.tsx create mode 100644 x-pack/plugins/monitoring/public/alerts/callout.tsx create mode 100644 x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx create mode 100644 x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/expression.tsx rename x-pack/plugins/monitoring/public/{components/alerts/index.js => alerts/cpu_usage_alert/index.ts} (79%) create mode 100644 x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/validation.tsx create mode 100644 x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_duration.tsx create mode 100644 x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_percentage.tsx rename x-pack/plugins/monitoring/public/{components/alerts/configuration => alerts/legacy_alert}/index.ts (81%) create mode 100644 x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx create mode 100644 x-pack/plugins/monitoring/public/alerts/lib/replace_tokens.tsx create mode 100644 x-pack/plugins/monitoring/public/alerts/lib/should_show_alert_badge.ts create mode 100644 x-pack/plugins/monitoring/public/alerts/panel.tsx create mode 100644 x-pack/plugins/monitoring/public/alerts/status.tsx delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/__tests__/map_severity.js delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/alerts.js delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.tsx delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/configuration/step1.tsx delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/configuration/step2.tsx delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/configuration/step3.tsx delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/formatted_alert.js delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/manage_email_action.tsx delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/map_severity.js delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/status.test.tsx delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/status.tsx delete mode 100644 x-pack/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js delete mode 100644 x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js delete mode 100644 x-pack/plugins/monitoring/public/components/cluster/overview/license_text.js delete mode 100644 x-pack/plugins/monitoring/public/views/alerts/index.html delete mode 100644 x-pack/plugins/monitoring/public/views/alerts/index.js create mode 100644 x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/alerts_factory.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/base_alert.test.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/base_alert.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts delete mode 100644 x-pack/plugins/monitoring/server/alerts/cluster_state.test.ts delete mode 100644 x-pack/plugins/monitoring/server/alerts/cluster_state.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/index.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts delete mode 100644 x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts delete mode 100644 x-pack/plugins/monitoring/server/alerts/license_expiration.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.test.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.test.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_default_email_address.test.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_default_email_address.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.test.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/map_legacy_severity.test.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/map_legacy_severity.ts delete mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/alerts/alerts.js create mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts delete mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/alerts/legacy_alerts.js create mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts rename x-pack/plugins/monitoring/server/routes/{index.js => index.ts} (67%) delete mode 100644 x-pack/test/functional/apps/monitoring/cluster/alerts.js diff --git a/x-pack/legacy/plugins/monitoring/index.ts b/x-pack/legacy/plugins/monitoring/index.ts index ee31a3037a0cb..f03e1ebc009f5 100644 --- a/x-pack/legacy/plugins/monitoring/index.ts +++ b/x-pack/legacy/plugins/monitoring/index.ts @@ -6,7 +6,6 @@ import Hapi from 'hapi'; import { config } from './config'; -import { KIBANA_ALERTING_ENABLED } from '../../../plugins/monitoring/common/constants'; /** * Invokes plugin modules to instantiate the Monitoring plugin for Kibana @@ -14,9 +13,6 @@ import { KIBANA_ALERTING_ENABLED } from '../../../plugins/monitoring/common/cons * @return {Object} Monitoring UI Kibana plugin object */ const deps = ['kibana', 'elasticsearch', 'xpack_main']; -if (KIBANA_ALERTING_ENABLED) { - deps.push(...['alerts', 'actions']); -} export const monitoring = (kibana: any) => { return new kibana.Plugin({ require: deps, diff --git a/x-pack/plugins/monitoring/common/constants.ts b/x-pack/plugins/monitoring/common/constants.ts index eeed7b4d5acf6..2c714080969e4 100644 --- a/x-pack/plugins/monitoring/common/constants.ts +++ b/x-pack/plugins/monitoring/common/constants.ts @@ -139,7 +139,7 @@ export const INDEX_PATTERN = '.monitoring-*-6-*,.monitoring-*-7-*'; export const INDEX_PATTERN_KIBANA = '.monitoring-kibana-6-*,.monitoring-kibana-7-*'; export const INDEX_PATTERN_LOGSTASH = '.monitoring-logstash-6-*,.monitoring-logstash-7-*'; export const INDEX_PATTERN_BEATS = '.monitoring-beats-6-*,.monitoring-beats-7-*'; -export const INDEX_ALERTS = '.monitoring-alerts-6,.monitoring-alerts-7'; +export const INDEX_ALERTS = '.monitoring-alerts-6*,.monitoring-alerts-7*'; export const INDEX_PATTERN_ELASTICSEARCH = '.monitoring-es-6-*,.monitoring-es-7-*'; // This is the unique token that exists in monitoring indices collected by metricbeat @@ -222,41 +222,54 @@ export const TELEMETRY_COLLECTION_INTERVAL = 86400000; * as the only way to see the new UI and actually run Kibana alerts. It will * be false until all alerts have been migrated, then it will be removed */ -export const KIBANA_ALERTING_ENABLED = false; +export const KIBANA_CLUSTER_ALERTS_ENABLED = false; /** * The prefix for all alert types used by monitoring */ -export const ALERT_TYPE_PREFIX = 'monitoring_'; +export const ALERT_PREFIX = 'monitoring_'; +export const ALERT_LICENSE_EXPIRATION = `${ALERT_PREFIX}alert_license_expiration`; +export const ALERT_CLUSTER_HEALTH = `${ALERT_PREFIX}alert_cluster_health`; +export const ALERT_CPU_USAGE = `${ALERT_PREFIX}alert_cpu_usage`; +export const ALERT_NODES_CHANGED = `${ALERT_PREFIX}alert_nodes_changed`; +export const ALERT_ELASTICSEARCH_VERSION_MISMATCH = `${ALERT_PREFIX}alert_elasticsearch_version_mismatch`; +export const ALERT_KIBANA_VERSION_MISMATCH = `${ALERT_PREFIX}alert_kibana_version_mismatch`; +export const ALERT_LOGSTASH_VERSION_MISMATCH = `${ALERT_PREFIX}alert_logstash_version_mismatch`; /** - * This is the alert type id for the license expiration alert - */ -export const ALERT_TYPE_LICENSE_EXPIRATION = `${ALERT_TYPE_PREFIX}alert_type_license_expiration`; -/** - * This is the alert type id for the cluster state alert + * A listing of all alert types */ -export const ALERT_TYPE_CLUSTER_STATE = `${ALERT_TYPE_PREFIX}alert_type_cluster_state`; +export const ALERTS = [ + ALERT_LICENSE_EXPIRATION, + ALERT_CLUSTER_HEALTH, + ALERT_CPU_USAGE, + ALERT_NODES_CHANGED, + ALERT_ELASTICSEARCH_VERSION_MISMATCH, + ALERT_KIBANA_VERSION_MISMATCH, + ALERT_LOGSTASH_VERSION_MISMATCH, +]; /** - * A listing of all alert types + * A list of all legacy alerts, which means they are powered by watcher */ -export const ALERT_TYPES = [ALERT_TYPE_LICENSE_EXPIRATION, ALERT_TYPE_CLUSTER_STATE]; +export const LEGACY_ALERTS = [ + ALERT_LICENSE_EXPIRATION, + ALERT_CLUSTER_HEALTH, + ALERT_NODES_CHANGED, + ALERT_ELASTICSEARCH_VERSION_MISMATCH, + ALERT_KIBANA_VERSION_MISMATCH, + ALERT_LOGSTASH_VERSION_MISMATCH, +]; /** * Matches the id for the built-in in email action type * See x-pack/plugins/actions/server/builtin_action_types/email.ts */ export const ALERT_ACTION_TYPE_EMAIL = '.email'; - -/** - * The number of alerts that have been migrated - */ -export const NUMBER_OF_MIGRATED_ALERTS = 2; - /** - * The advanced settings config name for the email address + * Matches the id for the built-in in log action type + * See x-pack/plugins/actions/server/builtin_action_types/log.ts */ -export const MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS = 'monitoring:alertingEmailAddress'; +export const ALERT_ACTION_TYPE_LOG = '.server-log'; export const ALERT_EMAIL_SERVICES = ['gmail', 'hotmail', 'icloud', 'outlook365', 'ses', 'yahoo']; diff --git a/x-pack/plugins/monitoring/server/alerts/enums.ts b/x-pack/plugins/monitoring/common/enums.ts similarity index 54% rename from x-pack/plugins/monitoring/server/alerts/enums.ts rename to x-pack/plugins/monitoring/common/enums.ts index ccff588743af1..74711b31756be 100644 --- a/x-pack/plugins/monitoring/server/alerts/enums.ts +++ b/x-pack/plugins/monitoring/common/enums.ts @@ -4,13 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -export enum AlertClusterStateState { +export enum AlertClusterHealthType { Green = 'green', Red = 'red', Yellow = 'yellow', } -export enum AlertCommonPerClusterMessageTokenType { +export enum AlertSeverity { + Success = 'success', + Danger = 'danger', + Warning = 'warning', +} + +export enum AlertMessageTokenType { Time = 'time', Link = 'link', + DocLink = 'docLink', +} + +export enum AlertParamType { + Duration = 'duration', + Percentage = 'percentage', } diff --git a/x-pack/plugins/monitoring/common/formatting.js b/x-pack/plugins/monitoring/common/formatting.js index a3b3ce07c8c76..b2a67b3cd48da 100644 --- a/x-pack/plugins/monitoring/common/formatting.js +++ b/x-pack/plugins/monitoring/common/formatting.js @@ -17,10 +17,10 @@ export const LARGE_ABBREVIATED = '0,0.[0]a'; * @param date Either a numeric Unix timestamp or a {@code Date} object * @returns The date formatted using 'LL LTS' */ -export function formatDateTimeLocal(date, useUTC = false) { +export function formatDateTimeLocal(date, useUTC = false, timezone = null) { return useUTC ? moment.utc(date).format('LL LTS') - : moment.tz(date, moment.tz.guess()).format('LL LTS'); + : moment.tz(date, timezone || moment.tz.guess()).format('LL LTS'); } /** diff --git a/x-pack/plugins/monitoring/common/types.ts b/x-pack/plugins/monitoring/common/types.ts new file mode 100644 index 0000000000000..f5dc85dce32e1 --- /dev/null +++ b/x-pack/plugins/monitoring/common/types.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { Alert } from '../../alerts/common'; +import { AlertParamType } from './enums'; + +export interface CommonBaseAlert { + type: string; + label: string; + paramDetails: CommonAlertParamDetails; + rawAlert: Alert; + isLegacy: boolean; +} + +export interface CommonAlertStatus { + exists: boolean; + enabled: boolean; + states: CommonAlertState[]; + alert: CommonBaseAlert; +} + +export interface CommonAlertState { + firing: boolean; + state: any; + meta: any; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface CommonAlertFilter {} + +export interface CommonAlertCpuUsageFilter extends CommonAlertFilter { + nodeUuid: string; +} + +export interface CommonAlertParamDetail { + label: string; + type: AlertParamType; +} + +export interface CommonAlertParamDetails { + [name: string]: CommonAlertParamDetail; +} + +export interface CommonAlertParams { + [name: string]: string | number; +} diff --git a/x-pack/plugins/monitoring/kibana.json b/x-pack/plugins/monitoring/kibana.json index 65dd4b373a71a..3b9e60124b034 100644 --- a/x-pack/plugins/monitoring/kibana.json +++ b/x-pack/plugins/monitoring/kibana.json @@ -3,8 +3,17 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["monitoring"], - "requiredPlugins": ["licensing", "features", "data", "navigation", "kibanaLegacy"], - "optionalPlugins": ["alerts", "actions", "infra", "telemetryCollectionManager", "usageCollection", "home", "cloud"], + "requiredPlugins": [ + "licensing", + "features", + "data", + "navigation", + "kibanaLegacy", + "triggers_actions_ui", + "alerts", + "actions" + ], + "optionalPlugins": ["infra", "telemetryCollectionManager", "usageCollection", "home", "cloud"], "server": true, "ui": true, "requiredBundles": ["kibanaUtils", "home", "alerts", "kibanaReact", "licenseManagement"] diff --git a/x-pack/plugins/monitoring/public/alerts/badge.tsx b/x-pack/plugins/monitoring/public/alerts/badge.tsx new file mode 100644 index 0000000000000..4518d2c56cabb --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/badge.tsx @@ -0,0 +1,179 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiContextMenu, + EuiPopover, + EuiBadge, + EuiFlexGrid, + EuiFlexItem, + EuiText, +} from '@elastic/eui'; +import { CommonAlertStatus, CommonAlertState } from '../../common/types'; +import { AlertSeverity } from '../../common/enums'; +// @ts-ignore +import { formatDateTimeLocal } from '../../common/formatting'; +import { AlertState } from '../../server/alerts/types'; +import { AlertPanel } from './panel'; +import { Legacy } from '../legacy_shims'; +import { isInSetupMode } from '../lib/setup_mode'; + +function getDateFromState(states: CommonAlertState[]) { + const timestamp = states[0].state.ui.triggeredMS; + const tz = Legacy.shims.uiSettings.get('dateFormat:tz'); + return formatDateTimeLocal(timestamp, false, tz === 'Browser' ? null : tz); +} + +export const numberOfAlertsLabel = (count: number) => `${count} alert${count > 1 ? 's' : ''}`; + +interface Props { + alerts: { [alertTypeId: string]: CommonAlertStatus }; +} +export const AlertsBadge: React.FC = (props: Props) => { + const [showPopover, setShowPopover] = React.useState(null); + const inSetupMode = isInSetupMode(); + const alerts = Object.values(props.alerts).filter(Boolean); + + if (alerts.length === 0) { + return null; + } + + const badges = []; + + if (inSetupMode) { + const button = ( + setShowPopover(true)} + > + {numberOfAlertsLabel(alerts.length)} + + ); + const panels = [ + { + id: 0, + title: i18n.translate('xpack.monitoring.alerts.badge.panelTitle', { + defaultMessage: 'Alerts', + }), + items: alerts.map(({ alert }, index) => { + return { + name: {alert.label}, + panel: index + 1, + }; + }), + }, + ...alerts.map((alertStatus, index) => { + return { + id: index + 1, + title: alertStatus.alert.label, + width: 400, + content: , + }; + }), + ]; + + badges.push( + setShowPopover(null)} + panelPaddingSize="none" + withTitle + anchorPosition="downLeft" + > + + + ); + } else { + const byType = { + [AlertSeverity.Danger]: [] as CommonAlertStatus[], + [AlertSeverity.Warning]: [] as CommonAlertStatus[], + [AlertSeverity.Success]: [] as CommonAlertStatus[], + }; + + for (const alert of alerts) { + for (const alertState of alert.states) { + const state = alertState.state as AlertState; + byType[state.ui.severity].push(alert); + } + } + + const typesToShow = [AlertSeverity.Danger, AlertSeverity.Warning]; + for (const type of typesToShow) { + const list = byType[type]; + if (list.length === 0) { + continue; + } + + const button = ( + setShowPopover(type)} + > + {numberOfAlertsLabel(list.length)} + + ); + + const panels = [ + { + id: 0, + title: `Alerts`, + items: list.map(({ alert, states }, index) => { + return { + name: ( + + +

{getDateFromState(states)}

+
+ {alert.label} +
+ ), + panel: index + 1, + }; + }), + }, + ...list.map((alertStatus, index) => { + return { + id: index + 1, + title: getDateFromState(alertStatus.states), + width: 400, + content: , + }; + }), + ]; + + badges.push( + setShowPopover(null)} + panelPaddingSize="none" + withTitle + anchorPosition="downLeft" + > + + + ); + } + } + + return ( + + {badges.map((badge, index) => ( + + {badge} + + ))} + + ); +}; diff --git a/x-pack/plugins/monitoring/public/alerts/callout.tsx b/x-pack/plugins/monitoring/public/alerts/callout.tsx new file mode 100644 index 0000000000000..748ec257ea765 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/callout.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { CommonAlertStatus } from '../../common/types'; +import { AlertSeverity } from '../../common/enums'; +import { replaceTokens } from './lib/replace_tokens'; +import { AlertMessage } from '../../server/alerts/types'; + +const TYPES = [ + { + severity: AlertSeverity.Warning, + color: 'warning', + label: i18n.translate('xpack.monitoring.alerts.callout.warningLabel', { + defaultMessage: 'Warning alert(s)', + }), + }, + { + severity: AlertSeverity.Danger, + color: 'danger', + label: i18n.translate('xpack.monitoring.alerts.callout.dangerLabel', { + defaultMessage: 'DAnger alert(s)', + }), + }, +]; + +interface Props { + alerts: { [alertTypeId: string]: CommonAlertStatus }; +} +export const AlertsCallout: React.FC = (props: Props) => { + const { alerts } = props; + + const callouts = TYPES.map((type) => { + const list = []; + for (const alertTypeId of Object.keys(alerts)) { + const alertInstance = alerts[alertTypeId]; + for (const { state } of alertInstance.states) { + if (state.ui.severity === type.severity) { + list.push(state); + } + } + } + + if (list.length) { + return ( + + +
    + {list.map((state, index) => { + const nextStepsUi = + state.ui.message.nextSteps && state.ui.message.nextSteps.length ? ( +
      + {state.ui.message.nextSteps.map( + (step: AlertMessage, nextStepIndex: number) => ( +
    • {replaceTokens(step)}
    • + ) + )} +
    + ) : null; + + return ( +
  • + {replaceTokens(state.ui.message)} + {nextStepsUi} +
  • + ); + })} +
+
+ +
+ ); + } + }); + return {callouts}; +}; diff --git a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx new file mode 100644 index 0000000000000..56cba83813a63 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.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; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; +import { validate } from './validation'; +import { ALERT_CPU_USAGE } from '../../../common/constants'; +import { Expression } from './expression'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { CpuUsageAlert } from '../../../server/alerts'; + +export function createCpuUsageAlertType(): AlertTypeModel { + const alert = new CpuUsageAlert(); + return { + id: ALERT_CPU_USAGE, + name: alert.label, + iconClass: 'bell', + alertParamsExpression: (props: any) => ( + + ), + validate, + defaultActionMessage: '{{context.internalFullMessage}}', + requiresAppContext: false, + }; +} diff --git a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/expression.tsx b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/expression.tsx new file mode 100644 index 0000000000000..7dc6155de529e --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/expression.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { EuiForm, EuiSpacer } from '@elastic/eui'; +import { CommonAlertParamDetails } from '../../../common/types'; +import { AlertParamDuration } from '../flyout_expressions/alert_param_duration'; +import { AlertParamType } from '../../../common/enums'; +import { AlertParamPercentage } from '../flyout_expressions/alert_param_percentage'; + +export interface Props { + alertParams: { [property: string]: any }; + setAlertParams: (property: string, value: any) => void; + setAlertProperty: (property: string, value: any) => void; + errors: { [key: string]: string[] }; + paramDetails: CommonAlertParamDetails; +} + +export const Expression: React.FC = (props) => { + const { alertParams, paramDetails, setAlertParams, errors } = props; + + const alertParamsUi = Object.keys(alertParams).map((alertParamName) => { + const details = paramDetails[alertParamName]; + const value = alertParams[alertParamName]; + + switch (details.type) { + case AlertParamType.Duration: + return ( + + ); + case AlertParamType.Percentage: + return ( + + ); + } + }); + + return ( + + {alertParamsUi} + + + ); +}; diff --git a/x-pack/plugins/monitoring/public/components/alerts/index.js b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/index.ts similarity index 79% rename from x-pack/plugins/monitoring/public/components/alerts/index.js rename to x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/index.ts index c4eda37c2b252..6ef31ee472c61 100644 --- a/x-pack/plugins/monitoring/public/components/alerts/index.js +++ b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { Alerts } from './alerts'; +export { createCpuUsageAlertType } from './cpu_usage_alert'; diff --git a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/validation.tsx b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/validation.tsx new file mode 100644 index 0000000000000..577ec12e634ed --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/validation.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ValidationResult } from '../../../../triggers_actions_ui/public/types'; + +export function validate(opts: any): ValidationResult { + const validationResult = { errors: {} }; + + const errors: { [key: string]: string[] } = { + duration: [], + threshold: [], + }; + if (!opts.duration) { + errors.duration.push( + i18n.translate('xpack.monitoring.alerts.cpuUsage.validation.duration', { + defaultMessage: 'A valid duration is required.', + }) + ); + } + if (isNaN(opts.threshold)) { + errors.threshold.push( + i18n.translate('xpack.monitoring.alerts.cpuUsage.validation.threshold', { + defaultMessage: 'A valid number is required.', + }) + ); + } + + validationResult.errors = errors; + return validationResult; +} diff --git a/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_duration.tsx b/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_duration.tsx new file mode 100644 index 0000000000000..23a9ea1facbc9 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_duration.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexItem, EuiFlexGroup, EuiFieldNumber, EuiSelect, EuiFormRow } from '@elastic/eui'; + +enum TIME_UNITS { + SECOND = 's', + MINUTE = 'm', + HOUR = 'h', + DAY = 'd', +} +function getTimeUnitLabel(timeUnit = TIME_UNITS.SECOND, timeValue = '0') { + switch (timeUnit) { + case TIME_UNITS.SECOND: + return i18n.translate('xpack.monitoring.alerts.flyoutExpressions.timeUnits.secondLabel', { + defaultMessage: '{timeValue, plural, one {second} other {seconds}}', + values: { timeValue }, + }); + case TIME_UNITS.MINUTE: + return i18n.translate('xpack.monitoring.alerts.flyoutExpressions.timeUnits.minuteLabel', { + defaultMessage: '{timeValue, plural, one {minute} other {minutes}}', + values: { timeValue }, + }); + case TIME_UNITS.HOUR: + return i18n.translate('xpack.monitoring.alerts.flyoutExpressions.timeUnits.hourLabel', { + defaultMessage: '{timeValue, plural, one {hour} other {hours}}', + values: { timeValue }, + }); + case TIME_UNITS.DAY: + return i18n.translate('xpack.monitoring.alerts.flyoutExpressions.timeUnits.dayLabel', { + defaultMessage: '{timeValue, plural, one {day} other {days}}', + values: { timeValue }, + }); + } +} + +// TODO: WHY does this not work? +// import { getTimeUnitLabel, TIME_UNITS } from '../../../triggers_actions_ui/public'; + +interface Props { + name: string; + duration: string; + label: string; + errors: string[]; + setAlertParams: (property: string, value: any) => void; +} + +const parseRegex = /(\d+)(\smhd)/; +export const AlertParamDuration: React.FC = (props: Props) => { + const { name, label, setAlertParams, errors } = props; + const parsed = parseRegex.exec(props.duration); + const defaultValue = parsed && parsed[1] ? parseInt(parsed[1], 10) : 1; + const defaultUnit = parsed && parsed[2] ? parsed[2] : TIME_UNITS.MINUTE; + const [value, setValue] = React.useState(defaultValue); + const [unit, setUnit] = React.useState(defaultUnit); + + const timeUnits = Object.values(TIME_UNITS).map((timeUnit) => ({ + value: timeUnit, + text: getTimeUnitLabel(timeUnit), + })); + + React.useEffect(() => { + setAlertParams(name, `${value}${unit}`); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [unit, value]); + + return ( + 0}> + + + { + let newValue = parseInt(e.target.value, 10); + if (isNaN(newValue)) { + newValue = 0; + } + setValue(newValue); + }} + /> + + + setUnit(e.target.value)} + options={timeUnits} + /> + + + + ); +}; diff --git a/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_percentage.tsx b/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_percentage.tsx new file mode 100644 index 0000000000000..352fb72557498 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_percentage.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { EuiFormRow, EuiFieldNumber, EuiText } from '@elastic/eui'; + +interface Props { + name: string; + percentage: number; + label: string; + errors: string[]; + setAlertParams: (property: string, value: any) => void; +} +export const AlertParamPercentage: React.FC = (props: Props) => { + const { name, label, setAlertParams, errors } = props; + const [value, setValue] = React.useState(props.percentage); + + return ( + 0}> + + % + + } + onChange={(e) => { + let newValue = parseInt(e.target.value, 10); + if (isNaN(newValue)) { + newValue = 0; + } + setValue(newValue); + setAlertParams(name, newValue); + }} + /> + + ); +}; diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/index.ts b/x-pack/plugins/monitoring/public/alerts/legacy_alert/index.ts similarity index 81% rename from x-pack/plugins/monitoring/public/components/alerts/configuration/index.ts rename to x-pack/plugins/monitoring/public/alerts/legacy_alert/index.ts index 7a96c6e324ab3..6370ed66f0c30 100644 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/index.ts +++ b/x-pack/plugins/monitoring/public/alerts/legacy_alert/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { AlertsConfiguration } from './configuration'; +export { createLegacyAlertTypes } from './legacy_alert'; diff --git a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx new file mode 100644 index 0000000000000..58b37e43085ff --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiTextColor, EuiSpacer } from '@elastic/eui'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; +import { LEGACY_ALERTS } from '../../../common/constants'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { BY_TYPE } from '../../../server/alerts'; + +export function createLegacyAlertTypes(): AlertTypeModel[] { + return LEGACY_ALERTS.map((legacyAlert) => { + const alertCls = BY_TYPE[legacyAlert]; + const alert = new alertCls(); + return { + id: legacyAlert, + name: alert.label, + iconClass: 'bell', + alertParamsExpression: (props: any) => ( + + + + {i18n.translate('xpack.monitoring.alerts.legacyAlert.expressionText', { + defaultMessage: 'There is nothing to configure.', + })} + + + + ), + defaultActionMessage: '{{context.internalFullMessage}}', + validate: () => ({ errors: {} }), + requiresAppContext: false, + }; + }); +} diff --git a/x-pack/plugins/monitoring/public/alerts/lib/replace_tokens.tsx b/x-pack/plugins/monitoring/public/alerts/lib/replace_tokens.tsx new file mode 100644 index 0000000000000..29e0822ad684d --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/lib/replace_tokens.tsx @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { Fragment } from 'react'; +import moment from 'moment'; +import { EuiLink } from '@elastic/eui'; +import { + AlertMessage, + AlertMessageTimeToken, + AlertMessageLinkToken, + AlertMessageDocLinkToken, +} from '../../../server/alerts/types'; +// @ts-ignore +import { formatTimestampToDuration } from '../../../common'; +import { CALCULATE_DURATION_UNTIL } from '../../../common/constants'; +import { AlertMessageTokenType } from '../../../common/enums'; +import { Legacy } from '../../legacy_shims'; + +export function replaceTokens(alertMessage: AlertMessage): JSX.Element | string | null { + if (!alertMessage) { + return null; + } + + let text = alertMessage.text; + if (!alertMessage.tokens || !alertMessage.tokens.length) { + return text; + } + + const timeTokens = alertMessage.tokens.filter( + (token) => token.type === AlertMessageTokenType.Time + ); + const linkTokens = alertMessage.tokens.filter( + (token) => token.type === AlertMessageTokenType.Link + ); + const docLinkTokens = alertMessage.tokens.filter( + (token) => token.type === AlertMessageTokenType.DocLink + ); + + for (const token of timeTokens) { + const timeToken = token as AlertMessageTimeToken; + text = text.replace( + timeToken.startToken, + timeToken.isRelative + ? formatTimestampToDuration(timeToken.timestamp, CALCULATE_DURATION_UNTIL) + : moment.tz(timeToken.timestamp, moment.tz.guess()).format('LLL z') + ); + } + + let element: JSX.Element = {text}; + for (const token of linkTokens) { + const linkToken = token as AlertMessageLinkToken; + const linkPart = new RegExp(`${linkToken.startToken}(.+?)${linkToken.endToken}`).exec(text); + if (!linkPart || linkPart.length < 2) { + continue; + } + const index = text.indexOf(linkPart[0]); + const preString = text.substring(0, index); + const postString = text.substring(index + linkPart[0].length); + element = ( + + {preString} + {linkPart[1]} + {postString} + + ); + } + + for (const token of docLinkTokens) { + const linkToken = token as AlertMessageDocLinkToken; + const linkPart = new RegExp(`${linkToken.startToken}(.+?)${linkToken.endToken}`).exec(text); + if (!linkPart || linkPart.length < 2) { + continue; + } + + const url = linkToken.partialUrl + .replace('{elasticWebsiteUrl}', Legacy.shims.docLinks.ELASTIC_WEBSITE_URL) + .replace('{docLinkVersion}', Legacy.shims.docLinks.DOC_LINK_VERSION); + const index = text.indexOf(linkPart[0]); + const preString = text.substring(0, index); + const postString = text.substring(index + linkPart[0].length); + element = ( + + {preString} + {linkPart[1]} + {postString} + + ); + } + + return element; +} diff --git a/x-pack/plugins/monitoring/public/alerts/lib/should_show_alert_badge.ts b/x-pack/plugins/monitoring/public/alerts/lib/should_show_alert_badge.ts new file mode 100644 index 0000000000000..c6773e9ca0156 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/lib/should_show_alert_badge.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { isInSetupMode } from '../../lib/setup_mode'; +import { CommonAlertStatus } from '../../../common/types'; + +export function shouldShowAlertBadge( + alerts: { [alertTypeId: string]: CommonAlertStatus }, + alertTypeIds: string[] +) { + const inSetupMode = isInSetupMode(); + return inSetupMode || alertTypeIds.find((name) => alerts[name] && alerts[name].states.length); +} diff --git a/x-pack/plugins/monitoring/public/alerts/panel.tsx b/x-pack/plugins/monitoring/public/alerts/panel.tsx new file mode 100644 index 0000000000000..3c5a4ef55a96b --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/panel.tsx @@ -0,0 +1,225 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiSpacer, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiSwitch, + EuiTitle, + EuiHorizontalRule, + EuiListGroup, + EuiListGroupItem, +} from '@elastic/eui'; + +import { CommonAlertStatus } from '../../common/types'; +import { AlertMessage } from '../../server/alerts/types'; +import { Legacy } from '../legacy_shims'; +import { replaceTokens } from './lib/replace_tokens'; +import { AlertsContextProvider } from '../../../triggers_actions_ui/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertEdit } from '../../../triggers_actions_ui/public'; +import { isInSetupMode, hideBottomBar, showBottomBar } from '../lib/setup_mode'; +import { BASE_ALERT_API_PATH } from '../../../alerts/common'; + +interface Props { + alert: CommonAlertStatus; +} +export const AlertPanel: React.FC = (props: Props) => { + const { + alert: { states, alert }, + } = props; + const [showFlyout, setShowFlyout] = React.useState(false); + const [isEnabled, setIsEnabled] = React.useState(alert.rawAlert.enabled); + const [isMuted, setIsMuted] = React.useState(alert.rawAlert.muteAll); + const [isSaving, setIsSaving] = React.useState(false); + const inSetupMode = isInSetupMode(); + + if (!alert.rawAlert) { + return null; + } + + async function disableAlert() { + setIsSaving(true); + try { + await Legacy.shims.http.post(`${BASE_ALERT_API_PATH}/alert/${alert.rawAlert.id}/_disable`); + } catch (err) { + Legacy.shims.toastNotifications.addDanger({ + title: i18n.translate('xpack.monitoring.alerts.panel.disableAlert.errorTitle', { + defaultMessage: `Unable to disable alert`, + }), + text: err.message, + }); + } + setIsSaving(false); + } + async function enableAlert() { + setIsSaving(true); + try { + await Legacy.shims.http.post(`${BASE_ALERT_API_PATH}/alert/${alert.rawAlert.id}/_enable`); + } catch (err) { + Legacy.shims.toastNotifications.addDanger({ + title: i18n.translate('xpack.monitoring.alerts.panel.enableAlert.errorTitle', { + defaultMessage: `Unable to enable alert`, + }), + text: err.message, + }); + } + setIsSaving(false); + } + async function muteAlert() { + setIsSaving(true); + try { + await Legacy.shims.http.post(`${BASE_ALERT_API_PATH}/alert/${alert.rawAlert.id}/_mute_all`); + } catch (err) { + Legacy.shims.toastNotifications.addDanger({ + title: i18n.translate('xpack.monitoring.alerts.panel.muteAlert.errorTitle', { + defaultMessage: `Unable to mute alert`, + }), + text: err.message, + }); + } + setIsSaving(false); + } + async function unmuteAlert() { + setIsSaving(true); + try { + await Legacy.shims.http.post(`${BASE_ALERT_API_PATH}/alert/${alert.rawAlert.id}/_unmute_all`); + } catch (err) { + Legacy.shims.toastNotifications.addDanger({ + title: i18n.translate('xpack.monitoring.alerts.panel.ummuteAlert.errorTitle', { + defaultMessage: `Unable to unmute alert`, + }), + text: err.message, + }); + } + setIsSaving(false); + } + + const flyoutUi = showFlyout ? ( + {}, + capabilities: Legacy.shims.capabilities, + }} + > + { + setShowFlyout(false); + showBottomBar(); + }} + /> + + ) : null; + + const configurationUi = ( + + + + { + setShowFlyout(true); + hideBottomBar(); + }} + > + {i18n.translate('xpack.monitoring.alerts.panel.editAlert', { + defaultMessage: `Edit alert`, + })} + + + + { + if (isEnabled) { + setIsEnabled(false); + await disableAlert(); + } else { + setIsEnabled(true); + await enableAlert(); + } + }} + label={ + + } + /> + + + { + if (isMuted) { + setIsMuted(false); + await unmuteAlert(); + } else { + setIsMuted(true); + await muteAlert(); + } + }} + label={ + + } + /> + + + {flyoutUi} + + ); + + if (inSetupMode) { + return
{configurationUi}
; + } + + const firingStates = states.filter((state) => state.firing); + if (!firingStates.length) { + return
{configurationUi}
; + } + + const firingState = firingStates[0]; + const nextStepsUi = + firingState.state.ui.message.nextSteps && firingState.state.ui.message.nextSteps.length ? ( + + {firingState.state.ui.message.nextSteps.map((step: AlertMessage, index: number) => ( + + ))} + + ) : null; + + return ( + +
+ +
{replaceTokens(firingState.state.ui.message)}
+
+ {nextStepsUi ? : null} + {nextStepsUi} +
+ +
{configurationUi}
+
+ ); +}; diff --git a/x-pack/plugins/monitoring/public/alerts/status.tsx b/x-pack/plugins/monitoring/public/alerts/status.tsx new file mode 100644 index 0000000000000..d15dcc9974863 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/status.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { EuiToolTip, EuiHealth } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { CommonAlertStatus } from '../../common/types'; +import { AlertSeverity } from '../../common/enums'; +import { AlertState } from '../../server/alerts/types'; +import { AlertsBadge } from './badge'; + +interface Props { + alerts: { [alertTypeId: string]: CommonAlertStatus }; + showBadge: boolean; + showOnlyCount: boolean; +} +export const AlertsStatus: React.FC = (props: Props) => { + const { alerts, showBadge = false, showOnlyCount = false } = props; + + let atLeastOneDanger = false; + const count = Object.values(alerts).reduce((cnt, alertStatus) => { + if (alertStatus.states.length) { + if (!atLeastOneDanger) { + for (const state of alertStatus.states) { + if ((state.state as AlertState).ui.severity === AlertSeverity.Danger) { + atLeastOneDanger = true; + break; + } + } + } + cnt++; + } + return cnt; + }, 0); + + if (count === 0) { + return ( + + + {showOnlyCount ? ( + count + ) : ( + + )} + + + ); + } + + if (showBadge) { + return ; + } + + const severity = atLeastOneDanger ? AlertSeverity.Danger : AlertSeverity.Warning; + + const tooltipText = (() => { + switch (severity) { + case AlertSeverity.Danger: + return i18n.translate('xpack.monitoring.alerts.status.highSeverityTooltip', { + defaultMessage: 'There are some critical issues that require your immediate attention!', + }); + case AlertSeverity.Warning: + return i18n.translate('xpack.monitoring.alerts.status.mediumSeverityTooltip', { + defaultMessage: 'There are some issues that might have impact on the stack.', + }); + default: + // might never show + return i18n.translate('xpack.monitoring.alerts.status.lowSeverityTooltip', { + defaultMessage: 'There are some low-severity issues.', + }); + } + })(); + + return ( + + + {showOnlyCount ? ( + count + ) : ( + + )} + + + ); +}; diff --git a/x-pack/plugins/monitoring/public/angular/app_modules.ts b/x-pack/plugins/monitoring/public/angular/app_modules.ts index 9ebb074ec7c3b..f3d77b196b26e 100644 --- a/x-pack/plugins/monitoring/public/angular/app_modules.ts +++ b/x-pack/plugins/monitoring/public/angular/app_modules.ts @@ -18,7 +18,7 @@ import { createTopNavDirective, createTopNavHelper, } from '../../../../../src/plugins/kibana_legacy/public'; -import { MonitoringPluginDependencies } from '../types'; +import { MonitoringStartPluginDependencies } from '../types'; import { GlobalState } from '../url_state'; import { getSafeForExternalLink } from '../lib/get_safe_for_external_link'; @@ -60,7 +60,7 @@ export const localAppModule = ({ data: { query }, navigation, externalConfig, -}: MonitoringPluginDependencies) => { +}: MonitoringStartPluginDependencies) => { createLocalI18nModule(); createLocalPrivateModule(); createLocalStorage(); @@ -90,7 +90,9 @@ export const localAppModule = ({ return appModule; }; -function createMonitoringAppConfigConstants(keys: MonitoringPluginDependencies['externalConfig']) { +function createMonitoringAppConfigConstants( + keys: MonitoringStartPluginDependencies['externalConfig'] +) { let constantsModule = angular.module('monitoring/constants', []); keys.map(([key, value]) => (constantsModule = constantsModule.constant(key as string, value))); } @@ -173,7 +175,7 @@ function createMonitoringAppFilters() { }); } -function createLocalConfigModule(core: MonitoringPluginDependencies['core']) { +function createLocalConfigModule(core: MonitoringStartPluginDependencies['core']) { angular.module('monitoring/Config', []).provider('config', function () { return { $get: () => ({ @@ -201,7 +203,7 @@ function createLocalPrivateModule() { angular.module('monitoring/Private', []).provider('Private', PrivateProvider); } -function createLocalTopNavModule({ ui }: MonitoringPluginDependencies['navigation']) { +function createLocalTopNavModule({ ui }: MonitoringStartPluginDependencies['navigation']) { angular .module('monitoring/TopNav', ['react']) .directive('kbnTopNav', createTopNavDirective) diff --git a/x-pack/plugins/monitoring/public/angular/index.ts b/x-pack/plugins/monitoring/public/angular/index.ts index 69d97a5e3bdc3..da57c028643a5 100644 --- a/x-pack/plugins/monitoring/public/angular/index.ts +++ b/x-pack/plugins/monitoring/public/angular/index.ts @@ -10,13 +10,13 @@ import { Legacy } from '../legacy_shims'; import { configureAppAngularModule } from '../../../../../src/plugins/kibana_legacy/public'; import { localAppModule, appModuleName } from './app_modules'; -import { MonitoringPluginDependencies } from '../types'; +import { MonitoringStartPluginDependencies } from '../types'; const APP_WRAPPER_CLASS = 'monApplicationWrapper'; export class AngularApp { private injector?: angular.auto.IInjectorService; - constructor(deps: MonitoringPluginDependencies) { + constructor(deps: MonitoringStartPluginDependencies) { const { core, element, @@ -25,6 +25,7 @@ export class AngularApp { isCloud, pluginInitializerContext, externalConfig, + triggersActionsUi, kibanaLegacy, } = deps; const app: IModule = localAppModule(deps); @@ -40,6 +41,7 @@ export class AngularApp { pluginInitializerContext, externalConfig, kibanaLegacy, + triggersActionsUi, }, this.injector ); diff --git a/x-pack/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap b/x-pack/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap deleted file mode 100644 index 5562d4bae9b14..0000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap +++ /dev/null @@ -1,65 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Status should render a flyout when clicking the link 1`] = ` - - - -

- Monitoring alerts -

-
- -

- Configure an email server and email address to receive alerts. -

-
-
- - - -
-`; - -exports[`Status should render a success message if all alerts have been migrated and in setup mode 1`] = ` - -

- - Want to make changes? Click here. - -

-
-`; - -exports[`Status should render without setup mode 1`] = ` - - -

- - Migrate cluster alerts to our new alerting platform. - -

-
- -
-`; diff --git a/x-pack/plugins/monitoring/public/components/alerts/__tests__/map_severity.js b/x-pack/plugins/monitoring/public/components/alerts/__tests__/map_severity.js deleted file mode 100644 index 8f454e7d765c4..0000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/__tests__/map_severity.js +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { mapSeverity } from '../map_severity'; - -describe('mapSeverity', () => { - it('maps [0, 1000) as low', () => { - const low = { - value: 'low', - color: 'warning', - iconType: 'iInCircle', - title: 'Low severity alert', - }; - - expect(mapSeverity(-1)).to.not.eql(low); - expect(mapSeverity(0)).to.eql(low); - expect(mapSeverity(1)).to.eql(low); - expect(mapSeverity(500)).to.eql(low); - expect(mapSeverity(998)).to.eql(low); - expect(mapSeverity(999)).to.eql(low); - expect(mapSeverity(1000)).to.not.eql(low); - }); - - it('maps [1000, 2000) as medium', () => { - const medium = { - value: 'medium', - color: 'warning', - iconType: 'alert', - title: 'Medium severity alert', - }; - - expect(mapSeverity(999)).to.not.eql(medium); - expect(mapSeverity(1000)).to.eql(medium); - expect(mapSeverity(1001)).to.eql(medium); - expect(mapSeverity(1500)).to.eql(medium); - expect(mapSeverity(1998)).to.eql(medium); - expect(mapSeverity(1999)).to.eql(medium); - expect(mapSeverity(2000)).to.not.eql(medium); - }); - - it('maps (-INF, 0) and [2000, +INF) as high', () => { - const high = { - value: 'high', - color: 'danger', - iconType: 'bell', - title: 'High severity alert', - }; - - expect(mapSeverity(-123412456)).to.eql(high); - expect(mapSeverity(-1)).to.eql(high); - expect(mapSeverity(0)).to.not.eql(high); - expect(mapSeverity(1999)).to.not.eql(high); - expect(mapSeverity(2000)).to.eql(high); - expect(mapSeverity(2001)).to.eql(high); - expect(mapSeverity(2500)).to.eql(high); - expect(mapSeverity(2998)).to.eql(high); - expect(mapSeverity(2999)).to.eql(high); - expect(mapSeverity(3000)).to.eql(high); - expect(mapSeverity(123412456)).to.eql(high); - }); -}); diff --git a/x-pack/plugins/monitoring/public/components/alerts/alerts.js b/x-pack/plugins/monitoring/public/components/alerts/alerts.js deleted file mode 100644 index 59e838c449a3b..0000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/alerts.js +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { Legacy } from '../../legacy_shims'; -import { upperFirst, get } from 'lodash'; -import { formatDateTimeLocal } from '../../../common/formatting'; -import { formatTimestampToDuration } from '../../../common'; -import { - CALCULATE_DURATION_SINCE, - EUI_SORT_DESCENDING, - ALERT_TYPE_LICENSE_EXPIRATION, - ALERT_TYPE_CLUSTER_STATE, -} from '../../../common/constants'; -import { mapSeverity } from './map_severity'; -import { FormattedAlert } from '../../components/alerts/formatted_alert'; -import { EuiMonitoringTable } from '../../components/table'; -import { EuiHealth, EuiIcon, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -const linkToCategories = { - 'elasticsearch/nodes': 'Elasticsearch Nodes', - 'elasticsearch/indices': 'Elasticsearch Indices', - 'kibana/instances': 'Kibana Instances', - 'logstash/instances': 'Logstash Nodes', - [ALERT_TYPE_LICENSE_EXPIRATION]: 'License expiration', - [ALERT_TYPE_CLUSTER_STATE]: 'Cluster state', -}; -const getColumns = (timezone) => [ - { - name: i18n.translate('xpack.monitoring.alerts.statusColumnTitle', { - defaultMessage: 'Status', - }), - field: 'status', - sortable: true, - render: (severity) => { - const severityIconDefaults = { - title: i18n.translate('xpack.monitoring.alerts.severityTitle.unknown', { - defaultMessage: 'Unknown', - }), - color: 'subdued', - value: i18n.translate('xpack.monitoring.alerts.severityValue.unknown', { - defaultMessage: 'N/A', - }), - }; - const severityIcon = { ...severityIconDefaults, ...mapSeverity(severity) }; - - return ( - - - {upperFirst(severityIcon.value)} - - - ); - }, - }, - { - name: i18n.translate('xpack.monitoring.alerts.resolvedColumnTitle', { - defaultMessage: 'Resolved', - }), - field: 'resolved_timestamp', - sortable: true, - render: (resolvedTimestamp) => { - const notResolvedLabel = i18n.translate('xpack.monitoring.alerts.notResolvedDescription', { - defaultMessage: 'Not Resolved', - }); - - const resolution = { - icon: null, - text: notResolvedLabel, - }; - - if (resolvedTimestamp) { - resolution.text = i18n.translate('xpack.monitoring.alerts.resolvedAgoDescription', { - defaultMessage: '{duration} ago', - values: { - duration: formatTimestampToDuration(resolvedTimestamp, CALCULATE_DURATION_SINCE), - }, - }); - } else { - resolution.icon = ; - } - - return ( - - {resolution.icon} {resolution.text} - - ); - }, - }, - { - name: i18n.translate('xpack.monitoring.alerts.messageColumnTitle', { - defaultMessage: 'Message', - }), - field: 'message', - sortable: true, - render: (_message, alert) => { - const message = get(alert, 'message.text', get(alert, 'message', '')); - return ( - - ); - }, - }, - { - name: i18n.translate('xpack.monitoring.alerts.categoryColumnTitle', { - defaultMessage: 'Category', - }), - field: 'category', - sortable: true, - render: (link) => - linkToCategories[link] - ? linkToCategories[link] - : i18n.translate('xpack.monitoring.alerts.categoryColumn.generalLabel', { - defaultMessage: 'General', - }), - }, - { - name: i18n.translate('xpack.monitoring.alerts.lastCheckedColumnTitle', { - defaultMessage: 'Last Checked', - }), - field: 'update_timestamp', - sortable: true, - render: (timestamp) => formatDateTimeLocal(timestamp, timezone), - }, - { - name: i18n.translate('xpack.monitoring.alerts.triggeredColumnTitle', { - defaultMessage: 'Triggered', - }), - field: 'timestamp', - sortable: true, - render: (timestamp) => - i18n.translate('xpack.monitoring.alerts.triggeredColumnValue', { - defaultMessage: '{timestamp} ago', - values: { - timestamp: formatTimestampToDuration(timestamp, CALCULATE_DURATION_SINCE), - }, - }), - }, -]; - -export const Alerts = ({ alerts, sorting, pagination, onTableChange }) => { - const alertsFlattened = alerts.map((alert) => ({ - ...alert, - status: get(alert, 'metadata.severity', get(alert, 'severity', 0)), - category: get(alert, 'metadata.link', get(alert, 'type', null)), - })); - - const injector = Legacy.shims.getAngularInjector(); - const timezone = injector.get('config').get('dateFormat:tz'); - - return ( - - ); -}; diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap b/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap deleted file mode 100644 index 429d19fbb887e..0000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap +++ /dev/null @@ -1,121 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Configuration shallow view should render step 1 1`] = ` - - - Create new email action... - , - "inputDisplay": - Create new email action... - , - "value": "__new__", - }, - ] - } - valueOfSelected="" - /> - -`; - -exports[`Configuration shallow view should render step 2 1`] = ` - - - - - -`; - -exports[`Configuration shallow view should render step 3 1`] = ` - - - Save - - -`; - -exports[`Configuration should render high level steps 1`] = ` -
- - - - - - - - - -
-`; diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap b/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap deleted file mode 100644 index cb1081c0c14da..0000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap +++ /dev/null @@ -1,301 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Step1 creating should render a create form 1`] = ` - - - - - -`; - -exports[`Step1 editing should allow for editing 1`] = ` - - -

- Edit the action below. -

-
- - -
-`; - -exports[`Step1 should render normally 1`] = ` - - - From: , Service: - , - "inputDisplay": - From: , Service: - , - "value": "1", - }, - Object { - "dropdownDisplay": - Create new email action... - , - "inputDisplay": - Create new email action... - , - "value": "__new__", - }, - ] - } - valueOfSelected="1" - /> - - - - - Edit - - - - - Test - - - - - Delete - - - - -`; - -exports[`Step1 testing should should a tooltip if there is no email address 1`] = ` - - - Test - - -`; - -exports[`Step1 testing should show a failed test error 1`] = ` - - - From: , Service: - , - "inputDisplay": - From: , Service: - , - "value": "1", - }, - Object { - "dropdownDisplay": - Create new email action... - , - "inputDisplay": - Create new email action... - , - "value": "__new__", - }, - ] - } - valueOfSelected="1" - /> - - - - - Edit - - - - - Test - - - - - Delete - - - - - -

- Very detailed error message -

-
-
-`; - -exports[`Step1 testing should show a successful test 1`] = ` - - - From: , Service: - , - "inputDisplay": - From: , Service: - , - "value": "1", - }, - Object { - "dropdownDisplay": - Create new email action... - , - "inputDisplay": - Create new email action... - , - "value": "__new__", - }, - ] - } - valueOfSelected="1" - /> - - - - - Edit - - - - - Test - - - - - Delete - - - - - -

- Looks good on our end! -

-
-
-`; diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap b/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap deleted file mode 100644 index bac183618b491..0000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap +++ /dev/null @@ -1,49 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Step2 should render normally 1`] = ` - - - - - -`; - -exports[`Step2 should show form errors 1`] = ` - - - - - -`; diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap b/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap deleted file mode 100644 index ed15ae9a9cff7..0000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap +++ /dev/null @@ -1,95 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Step3 should render normally 1`] = ` - - - Save - - -`; - -exports[`Step3 should show a disabled state 1`] = ` - - - Save - - -`; - -exports[`Step3 should show a saving state 1`] = ` - - - Save - - -`; - -exports[`Step3 should show an error 1`] = ` - - -

- Test error -

-
- - - Save - -
-`; diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx deleted file mode 100644 index 7caef8c230bf4..0000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { mockUseEffects } from '../../../jest.helpers'; -import { shallow, ShallowWrapper } from 'enzyme'; -import { Legacy } from '../../../legacy_shims'; -import { AlertsConfiguration, AlertsConfigurationProps } from './configuration'; - -jest.mock('../../../legacy_shims', () => ({ - Legacy: { - shims: { - kfetch: jest.fn(), - }, - }, -})); - -const defaultProps: AlertsConfigurationProps = { - emailAddress: 'test@elastic.co', - onDone: jest.fn(), -}; - -describe('Configuration', () => { - it('should render high level steps', () => { - const component = shallow(); - expect(component.find('EuiSteps').shallow()).toMatchSnapshot(); - }); - - function getStep(component: ShallowWrapper, index: number) { - return component.find('EuiSteps').shallow().find('EuiStep').at(index).children().shallow(); - } - - describe('shallow view', () => { - it('should render step 1', () => { - const component = shallow(); - const stepOne = getStep(component, 0); - expect(stepOne).toMatchSnapshot(); - }); - - it('should render step 2', () => { - const component = shallow(); - const stepTwo = getStep(component, 1); - expect(stepTwo).toMatchSnapshot(); - }); - - it('should render step 3', () => { - const component = shallow(); - const stepThree = getStep(component, 2); - expect(stepThree).toMatchSnapshot(); - }); - }); - - describe('selected action', () => { - const actionId = 'a123b'; - let component: ShallowWrapper; - beforeEach(async () => { - mockUseEffects(2); - - (Legacy.shims.kfetch as jest.Mock).mockImplementation(() => { - return { - data: [ - { - actionTypeId: '.email', - id: actionId, - config: {}, - }, - ], - }; - }); - - component = shallow(); - }); - - it('reflect in Step1', async () => { - const steps = component.find('EuiSteps').dive(); - expect(steps.find('EuiStep').at(0).prop('title')).toBe('Select email action'); - expect(steps.find('Step1').prop('selectedEmailActionId')).toBe(actionId); - }); - - it('should enable Step2', async () => { - const steps = component.find('EuiSteps').dive(); - expect(steps.find('Step2').prop('isDisabled')).toBe(false); - }); - - it('should enable Step3', async () => { - const steps = component.find('EuiSteps').dive(); - expect(steps.find('Step3').prop('isDisabled')).toBe(false); - }); - }); - - describe('edit action', () => { - let component: ShallowWrapper; - beforeEach(async () => { - (Legacy.shims.kfetch as jest.Mock).mockImplementation(() => { - return { - data: [], - }; - }); - - component = shallow(); - }); - - it('disable Step2', async () => { - const steps = component.find('EuiSteps').dive(); - expect(steps.find('Step2').prop('isDisabled')).toBe(true); - }); - - it('disable Step3', async () => { - const steps = component.find('EuiSteps').dive(); - expect(steps.find('Step3').prop('isDisabled')).toBe(true); - }); - }); - - describe('no email address', () => { - let component: ShallowWrapper; - beforeEach(async () => { - (Legacy.shims.kfetch as jest.Mock).mockImplementation(() => { - return { - data: [ - { - actionTypeId: '.email', - id: 'actionId', - config: {}, - }, - ], - }; - }); - - component = shallow(); - }); - - it('should disable Step3', async () => { - const steps = component.find('EuiSteps').dive(); - expect(steps.find('Step3').prop('isDisabled')).toBe(true); - }); - }); -}); diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.tsx deleted file mode 100644 index f248e20493a24..0000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.tsx +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { ReactNode } from 'react'; -import { EuiSteps } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { Legacy } from '../../../legacy_shims'; -import { ActionResult } from '../../../../../../plugins/actions/common'; -import { ALERT_ACTION_TYPE_EMAIL } from '../../../../common/constants'; -import { getMissingFieldErrors } from '../../../lib/form_validation'; -import { Step1 } from './step1'; -import { Step2 } from './step2'; -import { Step3 } from './step3'; - -export interface AlertsConfigurationProps { - emailAddress: string; - onDone: Function; -} - -export interface StepResult { - title: string; - children: ReactNode; - status: any; -} - -export interface AlertsConfigurationForm { - email: string | null; -} - -export const NEW_ACTION_ID = '__new__'; - -export const AlertsConfiguration: React.FC = ( - props: AlertsConfigurationProps -) => { - const { onDone } = props; - - const [emailActions, setEmailActions] = React.useState([]); - const [selectedEmailActionId, setSelectedEmailActionId] = React.useState(''); - const [editAction, setEditAction] = React.useState(null); - const [emailAddress, setEmailAddress] = React.useState(props.emailAddress); - const [formErrors, setFormErrors] = React.useState({ email: null }); - const [showFormErrors, setShowFormErrors] = React.useState(false); - const [isSaving, setIsSaving] = React.useState(false); - const [saveError, setSaveError] = React.useState(''); - - React.useEffect(() => { - async function fetchData() { - await fetchEmailActions(); - } - - fetchData(); - }, []); - - React.useEffect(() => { - setFormErrors(getMissingFieldErrors({ email: emailAddress }, { email: '' })); - }, [emailAddress]); - - async function fetchEmailActions() { - const kibanaActions = await Legacy.shims.kfetch({ - method: 'GET', - pathname: `/api/actions`, - }); - - const actions = kibanaActions.data.filter( - (action: ActionResult) => action.actionTypeId === ALERT_ACTION_TYPE_EMAIL - ); - if (actions.length > 0) { - setSelectedEmailActionId(actions[0].id); - } else { - setSelectedEmailActionId(NEW_ACTION_ID); - } - setEmailActions(actions); - } - - async function save() { - if (emailAddress.length === 0) { - setShowFormErrors(true); - return; - } - setIsSaving(true); - setShowFormErrors(false); - - try { - await Legacy.shims.kfetch({ - method: 'POST', - pathname: `/api/monitoring/v1/alerts`, - body: JSON.stringify({ selectedEmailActionId, emailAddress }), - }); - } catch (err) { - setIsSaving(false); - setSaveError( - err?.body?.message || - i18n.translate('xpack.monitoring.alerts.configuration.unknownError', { - defaultMessage: 'Something went wrong. Please consult the server logs.', - }) - ); - return; - } - - onDone(); - } - - function isStep2Disabled() { - return isStep2AndStep3Disabled(); - } - - function isStep3Disabled() { - return isStep2AndStep3Disabled() || !emailAddress || emailAddress.length === 0; - } - - function isStep2AndStep3Disabled() { - return !!editAction || !selectedEmailActionId || selectedEmailActionId === NEW_ACTION_ID; - } - - function getStep2Status() { - const isDisabled = isStep2AndStep3Disabled(); - - if (isDisabled) { - return 'disabled' as const; - } - - if (emailAddress && emailAddress.length) { - return 'complete' as const; - } - - return 'incomplete' as const; - } - - function getStep1Status() { - if (editAction) { - return 'incomplete' as const; - } - - return selectedEmailActionId ? ('complete' as const) : ('incomplete' as const); - } - - const steps = [ - { - title: emailActions.length - ? i18n.translate('xpack.monitoring.alerts.configuration.selectEmailAction', { - defaultMessage: 'Select email action', - }) - : i18n.translate('xpack.monitoring.alerts.configuration.createEmailAction', { - defaultMessage: 'Create email action', - }), - children: ( - await fetchEmailActions()} - emailActions={emailActions} - selectedEmailActionId={selectedEmailActionId} - setSelectedEmailActionId={setSelectedEmailActionId} - emailAddress={emailAddress} - editAction={editAction} - setEditAction={setEditAction} - /> - ), - status: getStep1Status(), - }, - { - title: i18n.translate('xpack.monitoring.alerts.configuration.setEmailAddress', { - defaultMessage: 'Set the email to receive alerts', - }), - status: getStep2Status(), - children: ( - - ), - }, - { - title: i18n.translate('xpack.monitoring.alerts.configuration.confirm', { - defaultMessage: 'Confirm and save', - }), - status: getStep2Status(), - children: ( - - ), - }, - ]; - - return ( -
- -
- ); -}; diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx deleted file mode 100644 index 1be66ce4ccfef..0000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx +++ /dev/null @@ -1,331 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { omit, pick } from 'lodash'; -import '../../../jest.helpers'; -import { shallow } from 'enzyme'; -import { GetStep1Props } from './step1'; -import { EmailActionData } from '../manage_email_action'; -import { ALERT_ACTION_TYPE_EMAIL } from '../../../../common/constants'; - -let Step1: React.FC; -let NEW_ACTION_ID: string; - -function setModules() { - Step1 = require('./step1').Step1; - NEW_ACTION_ID = require('./configuration').NEW_ACTION_ID; -} - -describe('Step1', () => { - const emailActions = [ - { - id: '1', - actionTypeId: '1abc', - name: 'Testing', - config: {}, - isPreconfigured: false, - }, - ]; - const selectedEmailActionId = emailActions[0].id; - const setSelectedEmailActionId = jest.fn(); - const emailAddress = 'test@test.com'; - const editAction = null; - const setEditAction = jest.fn(); - const onActionDone = jest.fn(); - - const defaultProps: GetStep1Props = { - onActionDone, - emailActions, - selectedEmailActionId, - setSelectedEmailActionId, - emailAddress, - editAction, - setEditAction, - }; - - beforeEach(() => { - jest.isolateModules(() => { - jest.doMock('../../../legacy_shims', () => ({ - Legacy: { - shims: { - kfetch: () => { - return {}; - }, - }, - }, - })); - setModules(); - }); - }); - - it('should render normally', () => { - const component = shallow(); - - expect(component).toMatchSnapshot(); - }); - - describe('creating', () => { - it('should render a create form', () => { - const customProps = { - emailActions: [], - selectedEmailActionId: NEW_ACTION_ID, - }; - - const component = shallow(); - - expect(component).toMatchSnapshot(); - }); - - it('should render the select box if at least one action exists', () => { - const customProps = { - emailActions: [ - { - id: 'foo', - actionTypeId: '.email', - name: '', - config: {}, - isPreconfigured: false, - }, - ], - selectedEmailActionId: NEW_ACTION_ID, - }; - - const component = shallow(); - expect(component.find('EuiSuperSelect').exists()).toBe(true); - }); - - it('should send up the create to the server', async () => { - const kfetch = jest.fn().mockImplementation(() => {}); - jest.isolateModules(() => { - jest.doMock('../../../legacy_shims', () => ({ - Legacy: { - shims: { - kfetch, - }, - }, - })); - setModules(); - }); - - const customProps = { - emailActions: [], - selectedEmailActionId: NEW_ACTION_ID, - }; - - const component = shallow(); - - const data: EmailActionData = { - service: 'gmail', - host: 'smtp.gmail.com', - port: 465, - secure: true, - from: 'test@test.com', - user: 'user@user.com', - password: 'password', - }; - - const createEmailAction: (data: EmailActionData) => void = component - .find('ManageEmailAction') - .prop('createEmailAction'); - createEmailAction(data); - - expect(kfetch).toHaveBeenCalledWith({ - method: 'POST', - pathname: `/api/actions/action`, - body: JSON.stringify({ - name: 'Email action for Stack Monitoring alerts', - actionTypeId: ALERT_ACTION_TYPE_EMAIL, - config: omit(data, ['user', 'password']), - secrets: pick(data, ['user', 'password']), - }), - }); - }); - }); - - describe('editing', () => { - it('should allow for editing', () => { - const customProps = { - editAction: emailActions[0], - }; - - const component = shallow(); - - expect(component).toMatchSnapshot(); - }); - - it('should send up the edit to the server', async () => { - const kfetch = jest.fn().mockImplementation(() => {}); - jest.isolateModules(() => { - jest.doMock('../../../legacy_shims', () => ({ - Legacy: { - shims: { - kfetch, - }, - }, - })); - setModules(); - }); - - const customProps = { - editAction: emailActions[0], - }; - - const component = shallow(); - - const data: EmailActionData = { - service: 'gmail', - host: 'smtp.gmail.com', - port: 465, - secure: true, - from: 'test@test.com', - user: 'user@user.com', - password: 'password', - }; - - const createEmailAction: (data: EmailActionData) => void = component - .find('ManageEmailAction') - .prop('createEmailAction'); - createEmailAction(data); - - expect(kfetch).toHaveBeenCalledWith({ - method: 'PUT', - pathname: `/api/actions/action/${emailActions[0].id}`, - body: JSON.stringify({ - name: emailActions[0].name, - config: omit(data, ['user', 'password']), - secrets: pick(data, ['user', 'password']), - }), - }); - }); - }); - - describe('testing', () => { - it('should allow for testing', async () => { - jest.isolateModules(() => { - jest.doMock('../../../legacy_shims', () => ({ - Legacy: { - shims: { - kfetch: jest.fn().mockImplementation((arg) => { - if (arg.pathname === '/api/actions/action/1/_execute') { - return { status: 'ok' }; - } - return {}; - }), - }, - }, - })); - setModules(); - }); - - const component = shallow(); - - expect(component.find('EuiButton').at(1).prop('isLoading')).toBe(false); - component.find('EuiButton').at(1).simulate('click'); - expect(component.find('EuiButton').at(1).prop('isLoading')).toBe(true); - await component.update(); - expect(component.find('EuiButton').at(1).prop('isLoading')).toBe(false); - }); - - it('should show a successful test', async () => { - jest.isolateModules(() => { - jest.doMock('../../../legacy_shims', () => ({ - Legacy: { - shims: { - kfetch: (arg: any) => { - if (arg.pathname === '/api/actions/action/1/_execute') { - return { status: 'ok' }; - } - return {}; - }, - }, - }, - })); - setModules(); - }); - - const component = shallow(); - - component.find('EuiButton').at(1).simulate('click'); - await component.update(); - expect(component).toMatchSnapshot(); - }); - - it('should show a failed test error', async () => { - jest.isolateModules(() => { - jest.doMock('../../../legacy_shims', () => ({ - Legacy: { - shims: { - kfetch: (arg: any) => { - if (arg.pathname === '/api/actions/action/1/_execute') { - return { message: 'Very detailed error message' }; - } - return {}; - }, - }, - }, - })); - setModules(); - }); - - const component = shallow(); - - component.find('EuiButton').at(1).simulate('click'); - await component.update(); - expect(component).toMatchSnapshot(); - }); - - it('should not allow testing if there is no email address', () => { - const customProps = { - emailAddress: '', - }; - const component = shallow(); - expect(component.find('EuiButton').at(1).prop('isDisabled')).toBe(true); - }); - - it('should should a tooltip if there is no email address', () => { - const customProps = { - emailAddress: '', - }; - const component = shallow(); - expect(component.find('EuiToolTip')).toMatchSnapshot(); - }); - }); - - describe('deleting', () => { - it('should send up the delete to the server', async () => { - const kfetch = jest.fn().mockImplementation(() => {}); - jest.isolateModules(() => { - jest.doMock('../../../legacy_shims', () => ({ - Legacy: { - shims: { - kfetch, - }, - }, - })); - setModules(); - }); - - const customProps = { - setSelectedEmailActionId: jest.fn(), - onActionDone: jest.fn(), - }; - const component = shallow(); - - await component.find('EuiButton').at(2).simulate('click'); - await component.update(); - - expect(kfetch).toHaveBeenCalledWith({ - method: 'DELETE', - pathname: `/api/actions/action/${emailActions[0].id}`, - }); - - expect(customProps.setSelectedEmailActionId).toHaveBeenCalledWith(''); - expect(customProps.onActionDone).toHaveBeenCalled(); - expect(component.find('EuiButton').at(2).prop('isLoading')).toBe(false); - }); - }); -}); diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/step1.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/step1.tsx deleted file mode 100644 index b3e6c079378ef..0000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/step1.tsx +++ /dev/null @@ -1,334 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment } from 'react'; -import { - EuiText, - EuiSpacer, - EuiPanel, - EuiFlexGroup, - EuiFlexItem, - EuiButton, - EuiSuperSelect, - EuiToolTip, - EuiCallOut, -} from '@elastic/eui'; -import { omit, pick } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { Legacy } from '../../../legacy_shims'; -import { ActionResult, BASE_ACTION_API_PATH } from '../../../../../../plugins/actions/common'; -import { ManageEmailAction, EmailActionData } from '../manage_email_action'; -import { ALERT_ACTION_TYPE_EMAIL } from '../../../../common/constants'; -import { NEW_ACTION_ID } from './configuration'; - -export interface GetStep1Props { - onActionDone: () => Promise; - emailActions: ActionResult[]; - selectedEmailActionId: string; - setSelectedEmailActionId: (id: string) => void; - emailAddress: string; - editAction: ActionResult | null; - setEditAction: (action: ActionResult | null) => void; -} - -export const Step1: React.FC = (props: GetStep1Props) => { - const [isTesting, setIsTesting] = React.useState(false); - const [isDeleting, setIsDeleting] = React.useState(false); - const [testingStatus, setTestingStatus] = React.useState(null); - const [fullTestingError, setFullTestingError] = React.useState(''); - - async function createEmailAction(data: EmailActionData) { - if (props.editAction) { - await Legacy.shims.kfetch({ - method: 'PUT', - pathname: `${BASE_ACTION_API_PATH}/action/${props.editAction.id}`, - body: JSON.stringify({ - name: props.editAction.name, - config: omit(data, ['user', 'password']), - secrets: pick(data, ['user', 'password']), - }), - }); - props.setEditAction(null); - } else { - await Legacy.shims.kfetch({ - method: 'POST', - pathname: `${BASE_ACTION_API_PATH}/action`, - body: JSON.stringify({ - name: i18n.translate('xpack.monitoring.alerts.configuration.emailAction.name', { - defaultMessage: 'Email action for Stack Monitoring alerts', - }), - actionTypeId: ALERT_ACTION_TYPE_EMAIL, - config: omit(data, ['user', 'password']), - secrets: pick(data, ['user', 'password']), - }), - }); - } - - await props.onActionDone(); - } - - async function deleteEmailAction(id: string) { - setIsDeleting(true); - - await Legacy.shims.kfetch({ - method: 'DELETE', - pathname: `${BASE_ACTION_API_PATH}/action/${id}`, - }); - - if (props.editAction && props.editAction.id === id) { - props.setEditAction(null); - } - if (props.selectedEmailActionId === id) { - props.setSelectedEmailActionId(''); - } - await props.onActionDone(); - setIsDeleting(false); - setTestingStatus(null); - } - - async function testEmailAction() { - setIsTesting(true); - setTestingStatus(null); - - const params = { - subject: 'Kibana alerting test configuration', - message: `This is a test for the configured email action for Kibana alerting.`, - to: [props.emailAddress], - }; - - const result = await Legacy.shims.kfetch({ - method: 'POST', - pathname: `${BASE_ACTION_API_PATH}/action/${props.selectedEmailActionId}/_execute`, - body: JSON.stringify({ params }), - }); - if (result.status === 'ok') { - setTestingStatus(true); - } else { - setTestingStatus(false); - setFullTestingError(result.message); - } - setIsTesting(false); - } - - function getTestButton() { - const isTestingDisabled = !props.emailAddress || props.emailAddress.length === 0; - const testBtn = ( - - {i18n.translate('xpack.monitoring.alerts.configuration.testConfiguration.buttonText', { - defaultMessage: 'Test', - })} - - ); - - if (isTestingDisabled) { - return ( - - {testBtn} - - ); - } - - return testBtn; - } - - if (props.editAction) { - return ( - - -

- {i18n.translate('xpack.monitoring.alerts.configuration.step1.editAction', { - defaultMessage: 'Edit the action below.', - })} -

-
- - await createEmailAction(data)} - cancel={() => props.setEditAction(null)} - isNew={false} - action={props.editAction} - /> -
- ); - } - - const newAction = ( - - {i18n.translate('xpack.monitoring.alerts.configuration.newActionDropdownDisplay', { - defaultMessage: 'Create new email action...', - })} - - ); - - const options = [ - ...props.emailActions.map((action) => { - const actionLabel = i18n.translate( - 'xpack.monitoring.alerts.configuration.selectAction.inputDisplay', - { - defaultMessage: 'From: {from}, Service: {service}', - values: { - service: action.config.service, - from: action.config.from, - }, - } - ); - - return { - value: action.id, - inputDisplay: {actionLabel}, - dropdownDisplay: {actionLabel}, - }; - }), - { - value: NEW_ACTION_ID, - inputDisplay: newAction, - dropdownDisplay: newAction, - }, - ]; - - let selectBox: React.ReactNode | null = ( - props.setSelectedEmailActionId(id)} - hasDividers - /> - ); - let createNew = null; - if (props.selectedEmailActionId === NEW_ACTION_ID) { - createNew = ( - - await createEmailAction(data)} - isNew={true} - /> - - ); - - // If there are no actions, do not show the select box as there are no choices - if (props.emailActions.length === 0) { - selectBox = null; - } else { - // Otherwise, add a spacer - selectBox = ( - - {selectBox} - - - ); - } - } - - let manageConfiguration = null; - const selectedEmailAction = props.emailActions.find( - (action) => action.id === props.selectedEmailActionId - ); - - if ( - props.selectedEmailActionId !== NEW_ACTION_ID && - props.selectedEmailActionId && - selectedEmailAction - ) { - let testingStatusUi = null; - if (testingStatus === true) { - testingStatusUi = ( - - - -

- {i18n.translate('xpack.monitoring.alerts.configuration.testConfiguration.success', { - defaultMessage: 'Looks good on our end!', - })} -

-
-
- ); - } else if (testingStatus === false) { - testingStatusUi = ( - - - -

{fullTestingError}

-
-
- ); - } - - manageConfiguration = ( - - - - - { - const editAction = - props.emailActions.find((action) => action.id === props.selectedEmailActionId) || - null; - props.setEditAction(editAction); - }} - > - {i18n.translate( - 'xpack.monitoring.alerts.configuration.editConfiguration.buttonText', - { - defaultMessage: 'Edit', - } - )} - - - {getTestButton()} - - deleteEmailAction(props.selectedEmailActionId)} - isLoading={isDeleting} - > - {i18n.translate( - 'xpack.monitoring.alerts.configuration.deleteConfiguration.buttonText', - { - defaultMessage: 'Delete', - } - )} - - - - {testingStatusUi} - - ); - } - - return ( - - {selectBox} - {manageConfiguration} - {createNew} - - ); -}; diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx deleted file mode 100644 index 14e3cb078f9cc..0000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import '../../../jest.helpers'; -import { shallow } from 'enzyme'; -import { Step2, GetStep2Props } from './step2'; - -describe('Step2', () => { - const defaultProps: GetStep2Props = { - emailAddress: 'test@test.com', - setEmailAddress: jest.fn(), - showFormErrors: false, - formErrors: { email: null }, - isDisabled: false, - }; - - it('should render normally', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - - it('should set the email address properly', () => { - const newEmail = 'email@email.com'; - const component = shallow(); - component.find('EuiFieldText').simulate('change', { target: { value: newEmail } }); - expect(defaultProps.setEmailAddress).toHaveBeenCalledWith(newEmail); - }); - - it('should show form errors', () => { - const customProps = { - showFormErrors: true, - formErrors: { - email: 'This is required', - }, - }; - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - - it('should disable properly', () => { - const customProps = { - isDisabled: true, - }; - const component = shallow(); - expect(component.find('EuiFieldText').prop('disabled')).toBe(true); - }); -}); diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/step2.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/step2.tsx deleted file mode 100644 index 2c215e310af69..0000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/step2.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { EuiForm, EuiFormRow, EuiFieldText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { AlertsConfigurationForm } from './configuration'; - -export interface GetStep2Props { - emailAddress: string; - setEmailAddress: (email: string) => void; - showFormErrors: boolean; - formErrors: AlertsConfigurationForm; - isDisabled: boolean; -} - -export const Step2: React.FC = (props: GetStep2Props) => { - return ( - - - props.setEmailAddress(e.target.value)} - /> - - - ); -}; diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx deleted file mode 100644 index 9b1304c42a507..0000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import '../../../jest.helpers'; -import { shallow } from 'enzyme'; -import { Step3 } from './step3'; - -describe('Step3', () => { - const defaultProps = { - isSaving: false, - isDisabled: false, - save: jest.fn(), - error: null, - }; - - it('should render normally', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - - it('should save properly', () => { - const component = shallow(); - component.find('EuiButton').simulate('click'); - expect(defaultProps.save).toHaveBeenCalledWith(); - }); - - it('should show a saving state', () => { - const customProps = { isSaving: true }; - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - - it('should show a disabled state', () => { - const customProps = { isDisabled: true }; - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - - it('should show an error', () => { - const customProps = { error: 'Test error' }; - const component = shallow(); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/step3.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/step3.tsx deleted file mode 100644 index 80acb8992cbc1..0000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/step3.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment } from 'react'; -import { EuiButton, EuiSpacer, EuiCallOut } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -export interface GetStep3Props { - isSaving: boolean; - isDisabled: boolean; - save: () => void; - error: string | null; -} - -export const Step3: React.FC = (props: GetStep3Props) => { - let errorUi = null; - if (props.error) { - errorUi = ( - - -

{props.error}

-
- -
- ); - } - - return ( - - {errorUi} - - {i18n.translate('xpack.monitoring.alerts.configuration.save', { - defaultMessage: 'Save', - })} - - - ); -}; diff --git a/x-pack/plugins/monitoring/public/components/alerts/formatted_alert.js b/x-pack/plugins/monitoring/public/components/alerts/formatted_alert.js deleted file mode 100644 index d23b5b60318c1..0000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/formatted_alert.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import moment from 'moment-timezone'; -import 'moment-duration-format'; -import React from 'react'; -import { formatTimestampToDuration } from '../../../common/format_timestamp_to_duration'; -import { CALCULATE_DURATION_UNTIL } from '../../../common/constants'; -import { EuiLink } from '@elastic/eui'; -import { getSafeForExternalLink } from '../../lib/get_safe_for_external_link'; - -export function FormattedAlert({ prefix, suffix, message, metadata }) { - const formattedAlert = (() => { - if (metadata && metadata.link) { - if (metadata.link.startsWith('https')) { - return ( - - {message} - - ); - } - - return ( - - {message} - - ); - } - - return message; - })(); - - if (metadata && metadata.time) { - // scan message prefix and replace relative times - // \w: Matches any alphanumeric character from the basic Latin alphabet, including the underscore. Equivalent to [A-Za-z0-9_]. - prefix = prefix.replace( - /{{#relativeTime}}metadata\.([\w\.]+){{\/relativeTime}}/, - (_match, field) => { - return formatTimestampToDuration(metadata[field], CALCULATE_DURATION_UNTIL); - } - ); - prefix = prefix.replace( - /{{#absoluteTime}}metadata\.([\w\.]+){{\/absoluteTime}}/, - (_match, field) => { - return moment.tz(metadata[field], moment.tz.guess()).format('LLL z'); - } - ); - } - - // suffix and prefix don't contain spaces - const formattedPrefix = prefix ? `${prefix} ` : null; - const formattedSuffix = suffix ? ` ${suffix}` : null; - return ( - - {formattedPrefix} - {formattedAlert} - {formattedSuffix} - - ); -} diff --git a/x-pack/plugins/monitoring/public/components/alerts/manage_email_action.tsx b/x-pack/plugins/monitoring/public/components/alerts/manage_email_action.tsx deleted file mode 100644 index 87588a435078d..0000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/manage_email_action.tsx +++ /dev/null @@ -1,301 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment } from 'react'; -import { - EuiForm, - EuiFormRow, - EuiFieldText, - EuiLink, - EuiSpacer, - EuiFieldNumber, - EuiFieldPassword, - EuiSwitch, - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiSuperSelect, - EuiText, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { ActionResult } from '../../../../../plugins/actions/common'; -import { getMissingFieldErrors, hasErrors, getRequiredFieldError } from '../../lib/form_validation'; -import { ALERT_EMAIL_SERVICES } from '../../../common/constants'; - -export interface EmailActionData { - service: string; - host: string; - port?: number; - secure: boolean; - from: string; - user: string; - password: string; -} - -interface ManageActionModalProps { - createEmailAction: (handler: EmailActionData) => void; - cancel?: () => void; - isNew: boolean; - action?: ActionResult | null; -} - -const DEFAULT_DATA: EmailActionData = { - service: '', - host: '', - port: 0, - secure: false, - from: '', - user: '', - password: '', -}; - -const CREATE_LABEL = i18n.translate('xpack.monitoring.alerts.migrate.manageAction.createLabel', { - defaultMessage: 'Create email action', -}); -const SAVE_LABEL = i18n.translate('xpack.monitoring.alerts.migrate.manageAction.saveLabel', { - defaultMessage: 'Save email action', -}); -const CANCEL_LABEL = i18n.translate('xpack.monitoring.alerts.migrate.manageAction.cancelLabel', { - defaultMessage: 'Cancel', -}); - -const NEW_SERVICE_ID = '__new__'; - -export const ManageEmailAction: React.FC = ( - props: ManageActionModalProps -) => { - const { createEmailAction, cancel, isNew, action } = props; - - const defaultData = Object.assign({}, DEFAULT_DATA, action ? action.config : {}); - const [isSaving, setIsSaving] = React.useState(false); - const [showErrors, setShowErrors] = React.useState(false); - const [errors, setErrors] = React.useState( - getMissingFieldErrors(defaultData, DEFAULT_DATA) - ); - const [data, setData] = React.useState(defaultData); - const [createNewService, setCreateNewService] = React.useState(false); - const [newService, setNewService] = React.useState(''); - - React.useEffect(() => { - const missingFieldErrors = getMissingFieldErrors(data, DEFAULT_DATA); - if (!missingFieldErrors.service) { - if (data.service === NEW_SERVICE_ID && !newService) { - missingFieldErrors.service = getRequiredFieldError('service'); - } - } - setErrors(missingFieldErrors); - }, [data, newService]); - - async function saveEmailAction() { - setShowErrors(true); - if (!hasErrors(errors)) { - setShowErrors(false); - setIsSaving(true); - const mergedData = { - ...data, - service: data.service === NEW_SERVICE_ID ? newService : data.service, - }; - try { - await createEmailAction(mergedData); - } catch (err) { - setErrors({ - general: err.body.message, - }); - } - } - } - - const serviceOptions = ALERT_EMAIL_SERVICES.map((service) => ({ - value: service, - inputDisplay: {service}, - dropdownDisplay: {service}, - })); - - serviceOptions.push({ - value: NEW_SERVICE_ID, - inputDisplay: ( - - {i18n.translate('xpack.monitoring.alerts.migrate.manageAction.addingNewServiceText', { - defaultMessage: 'Adding new service...', - })} - - ), - dropdownDisplay: ( - - {i18n.translate('xpack.monitoring.alerts.migrate.manageAction.addNewServiceText', { - defaultMessage: 'Add new service...', - })} - - ), - }); - - let addNewServiceUi = null; - if (createNewService) { - addNewServiceUi = ( - - - setNewService(e.target.value)} - isInvalid={showErrors} - /> - - ); - } - - return ( - - - {i18n.translate('xpack.monitoring.alerts.migrate.manageAction.serviceHelpText', { - defaultMessage: 'Find out more', - })} - - } - error={errors.service} - isInvalid={showErrors && !!errors.service} - > - - { - if (id === NEW_SERVICE_ID) { - setCreateNewService(true); - setData({ ...data, service: NEW_SERVICE_ID }); - } else { - setCreateNewService(false); - setData({ ...data, service: id }); - } - }} - hasDividers - isInvalid={showErrors && !!errors.service} - /> - {addNewServiceUi} - - - - - setData({ ...data, host: e.target.value })} - isInvalid={showErrors && !!errors.host} - /> - - - - setData({ ...data, port: parseInt(e.target.value, 10) })} - isInvalid={showErrors && !!errors.port} - /> - - - - setData({ ...data, secure: e.target.checked })} - /> - - - - setData({ ...data, from: e.target.value })} - isInvalid={showErrors && !!errors.from} - /> - - - - setData({ ...data, user: e.target.value })} - isInvalid={showErrors && !!errors.user} - /> - - - - setData({ ...data, password: e.target.value })} - isInvalid={showErrors && !!errors.password} - /> - - - - - - - - {isNew ? CREATE_LABEL : SAVE_LABEL} - - - {!action || isNew ? null : ( - - {CANCEL_LABEL} - - )} - - - ); -}; diff --git a/x-pack/plugins/monitoring/public/components/alerts/map_severity.js b/x-pack/plugins/monitoring/public/components/alerts/map_severity.js deleted file mode 100644 index 8232e0a8908d0..0000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/map_severity.js +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { upperFirst } from 'lodash'; - -/** - * Map the {@code severity} value to the associated alert level to be usable within the UI. - * - *
    - *
  1. Low: [0, 999) represents an informational level alert.
  2. - *
  3. Medium: [1000, 1999) represents a warning level alert.
  4. - *
  5. High: Any other value.
  6. - *
- * - * The object returned is in the form of: - * - * - * { - * value: 'medium', - * color: 'warning', - * iconType: 'dot', - * title: 'Warning severity alert' - * } - * - * - * @param {Number} severity The number representing the severity. Higher is "worse". - * @return {Object} An object containing details about the severity. - */ - -import { i18n } from '@kbn/i18n'; - -export function mapSeverity(severity) { - const floor = Math.floor(severity / 1000); - let mapped; - - switch (floor) { - case 0: - mapped = { - value: i18n.translate('xpack.monitoring.alerts.lowSeverityName', { defaultMessage: 'low' }), - color: 'warning', - iconType: 'iInCircle', - }; - break; - case 1: - mapped = { - value: i18n.translate('xpack.monitoring.alerts.mediumSeverityName', { - defaultMessage: 'medium', - }), - color: 'warning', - iconType: 'alert', - }; - break; - default: - // severity >= 2000 - mapped = { - value: i18n.translate('xpack.monitoring.alerts.highSeverityName', { - defaultMessage: 'high', - }), - color: 'danger', - iconType: 'bell', - }; - break; - } - - return { - title: i18n.translate('xpack.monitoring.alerts.severityTitle', { - defaultMessage: '{severity} severity alert', - values: { severity: upperFirst(mapped.value) }, - }), - ...mapped, - }; -} diff --git a/x-pack/plugins/monitoring/public/components/alerts/status.test.tsx b/x-pack/plugins/monitoring/public/components/alerts/status.test.tsx deleted file mode 100644 index 1c35328d2f881..0000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/status.test.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { shallow } from 'enzyme'; -import { Legacy } from '../../legacy_shims'; -import { AlertsStatus, AlertsStatusProps } from './status'; -import { ALERT_TYPES } from '../../../common/constants'; -import { getSetupModeState } from '../../lib/setup_mode'; -import { mockUseEffects } from '../../jest.helpers'; - -jest.mock('../../lib/setup_mode', () => ({ - getSetupModeState: jest.fn(), - addSetupModeCallback: jest.fn(), - toggleSetupMode: jest.fn(), -})); - -jest.mock('../../legacy_shims', () => ({ - Legacy: { - shims: { - kfetch: jest.fn(), - docLinks: { - ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', - DOC_LINK_VERSION: 'current', - }, - }, - }, -})); - -const defaultProps: AlertsStatusProps = { - clusterUuid: '1adsb23', - emailAddress: 'test@elastic.co', -}; - -describe('Status', () => { - beforeEach(() => { - mockUseEffects(2); - - (getSetupModeState as jest.Mock).mockReturnValue({ - enabled: false, - }); - - (Legacy.shims.kfetch as jest.Mock).mockImplementation(({ pathname }) => { - if (pathname === '/internal/security/api_key/privileges') { - return { areApiKeysEnabled: true }; - } - return { - data: [], - }; - }); - }); - - it('should render without setup mode', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - - it('should render a flyout when clicking the link', async () => { - (getSetupModeState as jest.Mock).mockReturnValue({ - enabled: true, - }); - - const component = shallow(); - component.find('EuiLink').simulate('click'); - await component.update(); - expect(component.find('EuiFlyout')).toMatchSnapshot(); - }); - - it('should render a success message if all alerts have been migrated and in setup mode', async () => { - (Legacy.shims.kfetch as jest.Mock).mockReturnValue({ - data: ALERT_TYPES.map((type) => ({ alertTypeId: type })), - }); - - (getSetupModeState as jest.Mock).mockReturnValue({ - enabled: true, - }); - - const component = shallow(); - await component.update(); - expect(component.find('EuiCallOut')).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/monitoring/public/components/alerts/status.tsx b/x-pack/plugins/monitoring/public/components/alerts/status.tsx deleted file mode 100644 index 6f72168f5069b..0000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/status.tsx +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment } from 'react'; -import { - EuiSpacer, - EuiCallOut, - EuiTitle, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiLink, - EuiText, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { Legacy } from '../../legacy_shims'; -import { Alert, BASE_ALERT_API_PATH } from '../../../../alerts/common'; -import { getSetupModeState, addSetupModeCallback, toggleSetupMode } from '../../lib/setup_mode'; -import { NUMBER_OF_MIGRATED_ALERTS, ALERT_TYPE_PREFIX } from '../../../common/constants'; -import { AlertsConfiguration } from './configuration'; - -export interface AlertsStatusProps { - clusterUuid: string; - emailAddress: string; -} - -export const AlertsStatus: React.FC = (props: AlertsStatusProps) => { - const { emailAddress } = props; - - const [setupModeEnabled, setSetupModeEnabled] = React.useState(getSetupModeState().enabled); - const [kibanaAlerts, setKibanaAlerts] = React.useState([]); - const [showMigrationFlyout, setShowMigrationFlyout] = React.useState(false); - const [isSecurityConfigured, setIsSecurityConfigured] = React.useState(false); - - React.useEffect(() => { - async function fetchAlertsStatus() { - const alerts = await Legacy.shims.kfetch({ - method: 'GET', - pathname: `${BASE_ALERT_API_PATH}/_find`, - }); - const monitoringAlerts = alerts.data.filter((alert: Alert) => - alert.alertTypeId.startsWith(ALERT_TYPE_PREFIX) - ); - setKibanaAlerts(monitoringAlerts); - } - - fetchAlertsStatus(); - fetchSecurityConfigured(); - }, [setupModeEnabled, showMigrationFlyout]); - - React.useEffect(() => { - if (!setupModeEnabled && showMigrationFlyout) { - setShowMigrationFlyout(false); - } - }, [setupModeEnabled, showMigrationFlyout]); - - async function fetchSecurityConfigured() { - const response = await Legacy.shims.kfetch({ - pathname: '/internal/security/api_key/privileges', - }); - setIsSecurityConfigured(response.areApiKeysEnabled); - } - - addSetupModeCallback(() => setSetupModeEnabled(getSetupModeState().enabled)); - - function enterSetupModeAndOpenFlyout() { - toggleSetupMode(true); - setShowMigrationFlyout(true); - } - - function getSecurityConfigurationErrorUi() { - if (isSecurityConfigured) { - return null; - } - const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; - const link = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/security-settings.html#api-key-service-settings`; - return ( - - - -

- - {i18n.translate( - 'xpack.monitoring.alerts.configuration.securityConfigurationError.docsLinkLabel', - { - defaultMessage: 'docs', - } - )} - - ), - }} - /> -

-
-
- ); - } - - function renderContent() { - let flyout = null; - if (showMigrationFlyout) { - flyout = ( - setShowMigrationFlyout(false)} aria-labelledby="flyoutTitle"> - - -

- {i18n.translate('xpack.monitoring.alerts.status.flyoutTitle', { - defaultMessage: 'Monitoring alerts', - })} -

-
- -

- {i18n.translate('xpack.monitoring.alerts.status.flyoutSubtitle', { - defaultMessage: 'Configure an email server and email address to receive alerts.', - })} -

-
- {getSecurityConfigurationErrorUi()} -
- - setShowMigrationFlyout(false)} - /> - -
- ); - } - - const allMigrated = kibanaAlerts.length >= NUMBER_OF_MIGRATED_ALERTS; - if (allMigrated) { - if (setupModeEnabled) { - return ( - - -

- - {i18n.translate('xpack.monitoring.alerts.status.manage', { - defaultMessage: 'Want to make changes? Click here.', - })} - -

-
- {flyout} -
- ); - } - } else { - return ( - - -

- - {i18n.translate('xpack.monitoring.alerts.status.needToMigrate', { - defaultMessage: 'Migrate cluster alerts to our new alerting platform.', - })} - -

-
- {flyout} -
- ); - } - } - - const content = renderContent(); - if (content) { - return ( - - {content} - - - ); - } - - return null; -}; diff --git a/x-pack/plugins/monitoring/public/components/chart/monitoring_timeseries_container.js b/x-pack/plugins/monitoring/public/components/chart/monitoring_timeseries_container.js index c6bd0773343e0..b760d35cfa2dc 100644 --- a/x-pack/plugins/monitoring/public/components/chart/monitoring_timeseries_container.js +++ b/x-pack/plugins/monitoring/public/components/chart/monitoring_timeseries_container.js @@ -23,6 +23,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { AlertsBadge } from '../../alerts/badge'; const zoomOutBtn = (zoomInfo) => { if (!zoomInfo || !zoomInfo.showZoomOutBtn()) { @@ -67,42 +68,56 @@ export function MonitoringTimeseriesContainer({ series, onBrush, zoomInfo }) { }), ].concat(series.map((item) => `${item.metric.label}: ${item.metric.description}`)); + let alertStatus = null; + if (series.alerts) { + alertStatus = ( + + + + ); + } + return ( - + - - - -

- {getTitle(series)} - {units ? ` (${units})` : ''} - - - - - -

-
-
+ - - } - /> - - - {seriesScreenReaderTextList.join('. ')} - - - + + + +

+ {getTitle(series)} + {units ? ` (${units})` : ''} + + + + + +

+
+
+ + + } + /> + + + {seriesScreenReaderTextList.join('. ')} + + + + + {zoomOutBtn(zoomInfo)} +
- {zoomOutBtn(zoomInfo)} + {alertStatus}
diff --git a/x-pack/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js b/x-pack/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js deleted file mode 100644 index 68d7a5a94e42f..0000000000000 --- a/x-pack/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { mapSeverity } from '../../alerts/map_severity'; -import { EuiHealth, EuiToolTip } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - -const HIGH_SEVERITY = 2000; -const MEDIUM_SEVERITY = 1000; -const LOW_SEVERITY = 0; - -export function AlertsIndicator({ alerts }) { - if (alerts && alerts.count > 0) { - const severity = (() => { - if (alerts.high > 0) { - return HIGH_SEVERITY; - } - if (alerts.medium > 0) { - return MEDIUM_SEVERITY; - } - return LOW_SEVERITY; - })(); - const severityIcon = mapSeverity(severity); - const tooltipText = (() => { - switch (severity) { - case HIGH_SEVERITY: - return i18n.translate( - 'xpack.monitoring.cluster.listing.alertsInticator.highSeverityTooltip', - { - defaultMessage: - 'There are some critical cluster issues that require your immediate attention!', - } - ); - case MEDIUM_SEVERITY: - return i18n.translate( - 'xpack.monitoring.cluster.listing.alertsInticator.mediumSeverityTooltip', - { - defaultMessage: 'There are some issues that might have impact on your cluster.', - } - ); - default: - // might never show - return i18n.translate( - 'xpack.monitoring.cluster.listing.alertsInticator.lowSeverityTooltip', - { - defaultMessage: 'There are some low-severity cluster issues', - } - ); - } - })(); - - return ( - - - - - - ); - } - - return ( - - - - - - ); -} diff --git a/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js b/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js index b90e7b52f4962..4dc4201e358fb 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js +++ b/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js @@ -14,16 +14,16 @@ import { EuiPage, EuiPageBody, EuiPageContent, - EuiToolTip, EuiCallOut, EuiSpacer, EuiIcon, + EuiToolTip, } from '@elastic/eui'; import { EuiMonitoringTable } from '../../table'; -import { AlertsIndicator } from '../../cluster/listing/alerts_indicator'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; +import { AlertsStatus } from '../../../alerts/status'; import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../../common/constants'; import './listing.scss'; @@ -31,8 +31,6 @@ const IsClusterSupported = ({ isSupported, children }) => { return isSupported ? children : '-'; }; -const STANDALONE_CLUSTER_STORAGE_KEY = 'viewedStandaloneCluster'; - /* * This checks if alerts feature is supported via monitoring cluster * license. If the alerts feature is not supported because the prod cluster @@ -61,6 +59,8 @@ const IsAlertsSupported = (props) => { ); }; +const STANDALONE_CLUSTER_STORAGE_KEY = 'viewedStandaloneCluster'; + const getColumns = ( showLicenseExpiration, changeCluster, @@ -119,7 +119,7 @@ const getColumns = ( render: (_status, cluster) => ( - + ), diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js deleted file mode 100644 index 2dc76aa7e4496..0000000000000 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment } from 'react'; -import moment from 'moment-timezone'; -import { FormattedAlert } from '../../alerts/formatted_alert'; -import { mapSeverity } from '../../alerts/map_severity'; -import { formatTimestampToDuration } from '../../../../common/format_timestamp_to_duration'; -import { - CALCULATE_DURATION_SINCE, - KIBANA_ALERTING_ENABLED, - CALCULATE_DURATION_UNTIL, -} from '../../../../common/constants'; -import { formatDateTimeLocal } from '../../../../common/formatting'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; - -import { - EuiFlexGroup, - EuiFlexItem, - EuiTitle, - EuiButton, - EuiText, - EuiSpacer, - EuiCallOut, - EuiLink, -} from '@elastic/eui'; - -function replaceTokens(alert) { - if (!alert.message.tokens) { - return alert.message.text; - } - - let text = alert.message.text; - - for (const token of alert.message.tokens) { - if (token.type === 'time') { - text = text.replace( - token.startToken, - token.isRelative - ? formatTimestampToDuration(alert.expirationTime, CALCULATE_DURATION_UNTIL) - : moment.tz(alert.expirationTime, moment.tz.guess()).format('LLL z') - ); - } else if (token.type === 'link') { - const linkPart = new RegExp(`${token.startToken}(.+?)${token.endToken}`).exec(text); - // TODO: we assume this is at the end, which works for now but will not always work - const nonLinkText = text.replace(linkPart[0], ''); - text = ( - - {nonLinkText} - {linkPart[1]} - - ); - } - } - - return text; -} - -export function AlertsPanel({ alerts }) { - if (!alerts || !alerts.length) { - // no-op - return null; - } - - // enclosed component for accessing - function TopAlertItem({ item, index }) { - const severityIcon = mapSeverity(item.metadata.severity); - - if (item.resolved_timestamp) { - severityIcon.title = i18n.translate( - 'xpack.monitoring.cluster.overview.alertsPanel.severityIconTitle', - { - defaultMessage: '{severityIconTitle} (resolved {time} ago)', - values: { - severityIconTitle: severityIcon.title, - time: formatTimestampToDuration(item.resolved_timestamp, CALCULATE_DURATION_SINCE), - }, - } - ); - severityIcon.color = 'success'; - severityIcon.iconType = 'check'; - } - - return ( - - - - -

- -

-
-
- ); - } - - const alertsList = KIBANA_ALERTING_ENABLED - ? alerts.map((alert, idx) => { - const callOutProps = mapSeverity(alert.severity); - const message = replaceTokens(alert); - - if (!alert.isFiring) { - callOutProps.title = i18n.translate( - 'xpack.monitoring.cluster.overview.alertsPanel.severityIconTitle', - { - defaultMessage: '{severityIconTitle} (resolved {time} ago)', - values: { - severityIconTitle: callOutProps.title, - time: formatTimestampToDuration(alert.resolvedMS, CALCULATE_DURATION_SINCE), - }, - } - ); - callOutProps.color = 'success'; - callOutProps.iconType = 'check'; - } - - return ( - - -

{message}

- - -

- -

-
-
- -
- ); - }) - : alerts.map((item, index) => ( - - )); - - return ( -
- - - -

- -

-
-
- - - - - -
- - {alertsList} - -
- ); -} diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js index 034bacfb3bf62..edf4c5d73f837 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js @@ -5,11 +5,11 @@ */ import React, { Fragment } from 'react'; +import moment from 'moment-timezone'; import { get, capitalize } from 'lodash'; import { formatNumber } from '../../../lib/format_number'; import { ClusterItemContainer, - HealthStatusIndicator, BytesPercentageUsage, DisabledIfNoDataAndInSetupModeLink, } from './helpers'; @@ -26,14 +26,24 @@ import { EuiBadge, EuiToolTip, EuiFlexGroup, + EuiHealth, + EuiText, } from '@elastic/eui'; -import { LicenseText } from './license_text'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { Reason } from '../../logs/reason'; import { SetupModeTooltip } from '../../setup_mode/tooltip'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; -import { ELASTICSEARCH_SYSTEM_ID } from '../../../../common/constants'; +import { + ELASTICSEARCH_SYSTEM_ID, + ALERT_LICENSE_EXPIRATION, + ALERT_CLUSTER_HEALTH, + ALERT_CPU_USAGE, + ALERT_NODES_CHANGED, + ALERT_ELASTICSEARCH_VERSION_MISMATCH, +} from '../../../../common/constants'; +import { AlertsBadge } from '../../../alerts/badge'; +import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; const calculateShards = (shards) => { const total = get(shards, 'total', 0); @@ -53,6 +63,8 @@ const calculateShards = (shards) => { }; }; +const formatDateLocal = (input) => moment.tz(input, moment.tz.guess()).format('LL'); + function getBadgeColorFromLogLevel(level) { switch (level) { case 'warn': @@ -138,11 +150,20 @@ function renderLog(log) { ); } +const OVERVIEW_PANEL_ALERTS = [ALERT_CLUSTER_HEALTH, ALERT_LICENSE_EXPIRATION]; + +const NODES_PANEL_ALERTS = [ + ALERT_CPU_USAGE, + ALERT_NODES_CHANGED, + ALERT_ELASTICSEARCH_VERSION_MISMATCH, +]; + export function ElasticsearchPanel(props) { const clusterStats = props.cluster_stats || {}; const nodes = clusterStats.nodes; const indices = clusterStats.indices; const setupMode = props.setupMode; + const alerts = props.alerts; const goToElasticsearch = () => getSafeForExternalLink('#/elasticsearch'); const goToNodes = () => getSafeForExternalLink('#/elasticsearch/nodes'); @@ -150,12 +171,6 @@ export function ElasticsearchPanel(props) { const { primaries, replicas } = calculateShards(get(props, 'cluster_stats.indices.shards', {})); - const statusIndicator = ; - - const licenseText = ( - - ); - const setupModeData = get(setupMode.data, 'elasticsearch'); const setupModeTooltip = setupMode && setupMode.enabled ? ( @@ -199,40 +214,80 @@ export function ElasticsearchPanel(props) { return null; }; + const statusColorMap = { + green: 'success', + yellow: 'warning', + red: 'danger', + }; + + let nodesAlertStatus = null; + if (shouldShowAlertBadge(alerts, NODES_PANEL_ALERTS)) { + const alertsList = NODES_PANEL_ALERTS.map((alertType) => alerts[alertType]); + nodesAlertStatus = ( + + + + ); + } + + let overviewAlertStatus = null; + if (shouldShowAlertBadge(alerts, OVERVIEW_PANEL_ALERTS)) { + const alertsList = OVERVIEW_PANEL_ALERTS.map((alertType) => alerts[alertType]); + overviewAlertStatus = ( + + + + ); + } + return ( - + - -

- - - -

-
+ + + +

+ + + +

+
+
+ {overviewAlertStatus} +
+ + + + + + + + {showMlJobs()} + + + + + + + + {capitalize(props.license.type)} + + + + + {props.license.expiry_date_in_millis === undefined ? ( + '' + ) : ( + + )} + + + +
- +

@@ -280,7 +365,12 @@ export function ElasticsearchPanel(props) {

- {setupModeTooltip} + + + {setupModeTooltip} + {nodesAlertStatus} + +
diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/helpers.js b/x-pack/plugins/monitoring/public/components/cluster/overview/helpers.js index 0d9290225cd5f..4f6fa520750bd 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/helpers.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/helpers.js @@ -29,13 +29,17 @@ export function HealthStatusIndicator(props) { const statusColor = statusColorMap[props.status] || 'n/a'; return ( - - - + + + + + + + ); } diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/index.js b/x-pack/plugins/monitoring/public/components/cluster/overview/index.js index 88c626b5ad5ae..66701c1dfd95a 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/index.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/index.js @@ -8,24 +8,14 @@ import React, { Fragment } from 'react'; import { ElasticsearchPanel } from './elasticsearch_panel'; import { KibanaPanel } from './kibana_panel'; import { LogstashPanel } from './logstash_panel'; -import { AlertsPanel } from './alerts_panel'; import { BeatsPanel } from './beats_panel'; import { EuiPage, EuiPageBody, EuiScreenReaderOnly } from '@elastic/eui'; import { ApmPanel } from './apm_panel'; import { FormattedMessage } from '@kbn/i18n/react'; -import { AlertsStatus } from '../../alerts/status'; -import { - STANDALONE_CLUSTER_CLUSTER_UUID, - KIBANA_ALERTING_ENABLED, -} from '../../../../common/constants'; +import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../../common/constants'; export function Overview(props) { const isFromStandaloneCluster = props.cluster.cluster_uuid === STANDALONE_CLUSTER_CLUSTER_UUID; - - const kibanaAlerts = KIBANA_ALERTING_ENABLED ? ( - - ) : null; - return ( @@ -38,10 +28,6 @@ export function Overview(props) {

- {kibanaAlerts} - - - {!isFromStandaloneCluster ? ( + - ) : null} - + diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js index 8bf2bc472b8fd..eb1f82eb5550d 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js @@ -28,11 +28,16 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { SetupModeTooltip } from '../../setup_mode/tooltip'; -import { KIBANA_SYSTEM_ID } from '../../../../common/constants'; +import { KIBANA_SYSTEM_ID, ALERT_KIBANA_VERSION_MISMATCH } from '../../../../common/constants'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; +import { AlertsBadge } from '../../../alerts/badge'; +import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; + +const INSTANCES_PANEL_ALERTS = [ALERT_KIBANA_VERSION_MISMATCH]; export function KibanaPanel(props) { const setupMode = props.setupMode; + const alerts = props.alerts; const showDetectedKibanas = setupMode.enabled && get(setupMode.data, 'kibana.detected.doesExist', false); if (!props.count && !showDetectedKibanas) { @@ -54,6 +59,16 @@ export function KibanaPanel(props) { /> ) : null; + let instancesAlertStatus = null; + if (shouldShowAlertBadge(alerts, INSTANCES_PANEL_ALERTS)) { + const alertsList = INSTANCES_PANEL_ALERTS.map((alertType) => alerts[alertType]); + instancesAlertStatus = ( + + + + ); + } + return ( - +

@@ -148,7 +163,12 @@ export function KibanaPanel(props) {

- {setupModeTooltip} + + + {setupModeTooltip} + {instancesAlertStatus} + +
diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/license_text.js b/x-pack/plugins/monitoring/public/components/cluster/overview/license_text.js deleted file mode 100644 index 19905b9d7791a..0000000000000 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/license_text.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import moment from 'moment-timezone'; -import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; -import { capitalize } from 'lodash'; -import { EuiLink } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -const formatDateLocal = (input) => moment.tz(input, moment.tz.guess()).format('LL'); - -export function LicenseText({ license, showLicenseExpiration }) { - if (!showLicenseExpiration) { - return null; - } - - return ( - - - ), - }} - /> - - ); -} diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js index e81f9b64dcb4b..7c9758bc0ddb6 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js @@ -11,7 +11,11 @@ import { BytesPercentageUsage, DisabledIfNoDataAndInSetupModeLink, } from './helpers'; -import { LOGSTASH, LOGSTASH_SYSTEM_ID } from '../../../../common/constants'; +import { + LOGSTASH, + LOGSTASH_SYSTEM_ID, + ALERT_LOGSTASH_VERSION_MISMATCH, +} from '../../../../common/constants'; import { EuiFlexGrid, @@ -31,11 +35,16 @@ import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; import { SetupModeTooltip } from '../../setup_mode/tooltip'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; +import { AlertsBadge } from '../../../alerts/badge'; +import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; + +const NODES_PANEL_ALERTS = [ALERT_LOGSTASH_VERSION_MISMATCH]; export function LogstashPanel(props) { const { setupMode } = props; const nodesCount = props.node_count || 0; const queueTypes = props.queue_types || {}; + const alerts = props.alerts; // Do not show if we are not in setup mode if (!nodesCount && !setupMode.enabled) { @@ -56,6 +65,16 @@ export function LogstashPanel(props) { /> ) : null; + let nodesAlertStatus = null; + if (shouldShowAlertBadge(alerts, NODES_PANEL_ALERTS)) { + const alertsList = NODES_PANEL_ALERTS.map((alertType) => alerts[alertType]); + nodesAlertStatus = ( + + + + ); + } + return ( - +

@@ -141,7 +160,12 @@ export function LogstashPanel(props) {

- {setupModeTooltip} + + + {setupModeTooltip} + {nodesAlertStatus} + +
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js index aea2456a3f3d4..ba19ed0ae1913 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js @@ -10,7 +10,7 @@ import { ElasticsearchStatusIcon } from '../status_icon'; import { formatMetric } from '../../../lib/format_number'; import { i18n } from '@kbn/i18n'; -export function ClusterStatus({ stats }) { +export function ClusterStatus({ stats, alerts }) { const { dataSize, nodesCount, @@ -81,6 +81,7 @@ export function ClusterStatus({ stats }) { diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js index 418661ff322e4..f91e251030d76 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js @@ -5,6 +5,7 @@ */ import React from 'react'; +import { get } from 'lodash'; import { EuiPage, EuiPageContent, @@ -20,8 +21,33 @@ import { Logs } from '../../logs/'; import { MonitoringTimeseriesContainer } from '../../chart'; import { ShardAllocation } from '../shard_allocation/shard_allocation'; import { FormattedMessage } from '@kbn/i18n/react'; +import { AlertsCallout } from '../../../alerts/callout'; + +export const Node = ({ + nodeSummary, + metrics, + logs, + alerts, + nodeId, + clusterUuid, + scope, + ...props +}) => { + if (alerts) { + for (const alertTypeId of Object.keys(alerts)) { + const alertInstance = alerts[alertTypeId]; + for (const { meta } of alertInstance.states) { + const metricList = get(meta, 'metrics', []); + for (const metric of metricList) { + if (metrics[metric]) { + metrics[metric].alerts = metrics[metric].alerts || {}; + metrics[metric].alerts[alertTypeId] = alertInstance; + } + } + } + } + } -export const Node = ({ nodeSummary, metrics, logs, nodeId, clusterUuid, scope, ...props }) => { const metricsToShow = [ metrics.node_jvm_mem, metrics.node_mem, @@ -31,6 +57,7 @@ export const Node = ({ nodeSummary, metrics, logs, nodeId, clusterUuid, scope, . metrics.node_latency, metrics.node_segment_count, ]; + return ( @@ -43,9 +70,10 @@ export const Node = ({ nodeSummary, metrics, logs, nodeId, clusterUuid, scope, .

- + + {metricsToShow.map((metric, index) => ( diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js index f912d2755b0c7..18533b3bd4b5e 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js @@ -10,7 +10,7 @@ import { NodeStatusIcon } from '../node'; import { formatMetric } from '../../../lib/format_number'; import { i18n } from '@kbn/i18n'; -export function NodeDetailStatus({ stats }) { +export function NodeDetailStatus({ stats, alerts }) { const { transport_address: transportAddress, usedHeap, @@ -28,6 +28,10 @@ export function NodeDetailStatus({ stats }) { const percentSpaceUsed = (freeSpace / totalSpace) * 100; const metrics = [ + { + label: 'Alerts', + value: {Object.values(alerts).length}, + }, { label: i18n.translate('xpack.monitoring.elasticsearch.nodeDetailStatus.transportAddress', { defaultMessage: 'Transport Address', diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js index 8844388f8647a..c2e5c8e22a1c0 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js @@ -5,7 +5,6 @@ */ import React, { Fragment } from 'react'; -import { NodeStatusIcon } from '../node'; import { extractIp } from '../../../lib/extract_ip'; // TODO this is only used for elasticsearch nodes summary / node detail, so it should be moved to components/elasticsearch/nodes/lib import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { ClusterStatus } from '../cluster_status'; @@ -25,12 +24,14 @@ import { EuiButton, EuiText, EuiScreenReaderOnly, + EuiHealth, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import _ from 'lodash'; import { ELASTICSEARCH_SYSTEM_ID } from '../../../../common/constants'; import { FormattedMessage } from '@kbn/i18n/react'; import { ListingCallOut } from '../../setup_mode/listing_callout'; +import { AlertsStatus } from '../../../alerts/status'; const getNodeTooltip = (node) => { const { nodeTypeLabel, nodeTypeClass } = node; @@ -56,7 +57,7 @@ const getNodeTooltip = (node) => { }; const getSortHandler = (type) => (item) => _.get(item, [type, 'summary', 'lastVal']); -const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => { +const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, alerts) => { const cols = []; const cpuUsageColumnTitle = i18n.translate( @@ -123,6 +124,18 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => { }, }); + cols.push({ + name: i18n.translate('xpack.monitoring.elasticsearch.nodes.alertsColumnTitle', { + defaultMessage: 'Alerts', + }), + field: 'alerts', + width: '175px', + sortable: true, + render: () => { + return ; + }, + }); + cols.push({ name: i18n.translate('xpack.monitoring.elasticsearch.nodes.statusColumnTitle', { defaultMessage: 'Status', @@ -138,9 +151,20 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => { defaultMessage: 'Offline', }); return ( -
- {status} -
+ + + {status} + + ); }, }); @@ -197,14 +221,16 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => { name: cpuUsageColumnTitle, field: 'node_cpu_utilization', sortable: getSortHandler('node_cpu_utilization'), - render: (value, node) => ( - - ), + render: (value, node) => { + return ( + + ); + }, }); cols.push({ @@ -263,8 +289,17 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => { }; export function ElasticsearchNodes({ clusterStatus, showCgroupMetricsElasticsearch, ...props }) { - const { sorting, pagination, onTableChange, clusterUuid, setupMode, fetchMoreData } = props; - const columns = getColumns(showCgroupMetricsElasticsearch, setupMode, clusterUuid); + const { + sorting, + pagination, + onTableChange, + clusterUuid, + setupMode, + fetchMoreData, + alerts, + } = props; + + const columns = getColumns(showCgroupMetricsElasticsearch, setupMode, clusterUuid, alerts); // Merge the nodes data with the setup data if enabled const nodes = props.nodes || []; @@ -392,7 +427,7 @@ export function ElasticsearchNodes({ clusterStatus, showCgroupMetricsElasticsear return ( - + diff --git a/x-pack/plugins/monitoring/public/components/kibana/cluster_status/index.js b/x-pack/plugins/monitoring/public/components/kibana/cluster_status/index.js index c9b95eb4876d8..32d2bdadcea96 100644 --- a/x-pack/plugins/monitoring/public/components/kibana/cluster_status/index.js +++ b/x-pack/plugins/monitoring/public/components/kibana/cluster_status/index.js @@ -10,7 +10,7 @@ import { KibanaStatusIcon } from '../status_icon'; import { formatMetric } from '../../../lib/format_number'; import { i18n } from '@kbn/i18n'; -export function ClusterStatus({ stats }) { +export function ClusterStatus({ stats, alerts }) { const { concurrent_connections: connections, count: instances, @@ -65,6 +65,7 @@ export function ClusterStatus({ stats }) { diff --git a/x-pack/plugins/monitoring/public/components/kibana/instances/instances.js b/x-pack/plugins/monitoring/public/components/kibana/instances/instances.js index 9f960c8ddea09..95a9276569bb1 100644 --- a/x-pack/plugins/monitoring/public/components/kibana/instances/instances.js +++ b/x-pack/plugins/monitoring/public/components/kibana/instances/instances.js @@ -14,11 +14,12 @@ import { EuiLink, EuiCallOut, EuiScreenReaderOnly, + EuiToolTip, + EuiHealth, } from '@elastic/eui'; import { capitalize, get } from 'lodash'; import { ClusterStatus } from '../cluster_status'; import { EuiMonitoringTable } from '../../table'; -import { KibanaStatusIcon } from '../status_icon'; import { StatusIcon } from '../../status_icon'; import { formatMetric, formatNumber } from '../../../lib/format_number'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; @@ -27,8 +28,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { SetupModeBadge } from '../../setup_mode/badge'; import { KIBANA_SYSTEM_ID } from '../../../../common/constants'; import { ListingCallOut } from '../../setup_mode/listing_callout'; +import { AlertsStatus } from '../../../alerts/status'; -const getColumns = (setupMode) => { +const getColumns = (setupMode, alerts) => { const columns = [ { name: i18n.translate('xpack.monitoring.kibana.listing.nameColumnTitle', { @@ -79,33 +81,34 @@ const getColumns = (setupMode) => { ); }, }, + { + name: i18n.translate('xpack.monitoring.kibana.listing.alertsColumnTitle', { + defaultMessage: 'Alerts', + }), + field: 'isOnline', + width: '175px', + sortable: true, + render: () => { + return ; + }, + }, { name: i18n.translate('xpack.monitoring.kibana.listing.statusColumnTitle', { defaultMessage: 'Status', }), field: 'status', - render: (status, kibana) => ( -
- -   - {!kibana.availability ? ( - - ) : ( - capitalize(status) - )} -
- ), + render: (status, kibana) => { + return ( + + + {capitalize(status)} + + + ); + }, }, { name: i18n.translate('xpack.monitoring.kibana.listing.loadAverageColumnTitle', { @@ -158,7 +161,7 @@ const getColumns = (setupMode) => { export class KibanaInstances extends PureComponent { render() { - const { clusterStatus, setupMode, sorting, pagination, onTableChange } = this.props; + const { clusterStatus, alerts, setupMode, sorting, pagination, onTableChange } = this.props; let setupModeCallOut = null; // Merge the instances data with the setup data if enabled @@ -254,7 +257,7 @@ export class KibanaInstances extends PureComponent { - + {setupModeCallOut} @@ -262,7 +265,7 @@ export class KibanaInstances extends PureComponent { ({ Legacy: { shims: { getBasePath: () => '', - capabilities: { - get: () => ({ logs: { show: true } }), - }, + capabilities: { logs: { show: true } }, }, }, })); diff --git a/x-pack/plugins/monitoring/public/components/logstash/cluster_status/index.js b/x-pack/plugins/monitoring/public/components/logstash/cluster_status/index.js index 9d5a6a184b4e8..abd18b61da8ff 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/cluster_status/index.js +++ b/x-pack/plugins/monitoring/public/components/logstash/cluster_status/index.js @@ -9,7 +9,7 @@ import { SummaryStatus } from '../../summary_status'; import { formatMetric } from '../../../lib/format_number'; import { i18n } from '@kbn/i18n'; -export function ClusterStatus({ stats }) { +export function ClusterStatus({ stats, alerts }) { const { node_count: nodeCount, avg_memory_used: avgMemoryUsed, @@ -49,5 +49,5 @@ export function ClusterStatus({ stats }) { }, ]; - return ; + return ; } diff --git a/x-pack/plugins/monitoring/public/components/logstash/listing/__snapshots__/listing.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/listing/__snapshots__/listing.test.js.snap index edb7d139bb935..2e01fce7247dc 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/listing/__snapshots__/listing.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/logstash/listing/__snapshots__/listing.test.js.snap @@ -11,6 +11,13 @@ exports[`Listing should render with certain data pieces missing 1`] = ` "render": [Function], "sortable": true, }, + Object { + "field": "isOnline", + "name": "Alerts", + "render": [Function], + "sortable": true, + "width": "175px", + }, Object { "field": "cpu_usage", "name": "CPU Usage", @@ -106,6 +113,13 @@ exports[`Listing should render with expected props 1`] = ` "render": [Function], "sortable": true, }, + Object { + "field": "isOnline", + "name": "Alerts", + "render": [Function], + "sortable": true, + "width": "175px", + }, Object { "field": "cpu_usage", "name": "CPU Usage", diff --git a/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js index 78eb982a95dd7..caa21e5e69292 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js +++ b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js @@ -16,7 +16,7 @@ import { EuiScreenReaderOnly, } from '@elastic/eui'; import { formatPercentageUsage, formatNumber } from '../../../lib/format_number'; -import { ClusterStatus } from '..//cluster_status'; +import { ClusterStatus } from '../cluster_status'; import { EuiMonitoringTable } from '../../table'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -24,10 +24,12 @@ import { LOGSTASH_SYSTEM_ID } from '../../../../common/constants'; import { SetupModeBadge } from '../../setup_mode/badge'; import { ListingCallOut } from '../../setup_mode/listing_callout'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; +import { AlertsStatus } from '../../../alerts/status'; export class Listing extends PureComponent { getColumns() { const setupMode = this.props.setupMode; + const alerts = this.props.alerts; return [ { @@ -72,6 +74,17 @@ export class Listing extends PureComponent { ); }, }, + { + name: i18n.translate('xpack.monitoring.logstash.nodes.alertsColumnTitle', { + defaultMessage: 'Alerts', + }), + field: 'isOnline', + width: '175px', + sortable: true, + render: () => { + return ; + }, + }, { name: i18n.translate('xpack.monitoring.logstash.nodes.cpuUsageTitle', { defaultMessage: 'CPU Usage', @@ -141,7 +154,7 @@ export class Listing extends PureComponent { } render() { - const { stats, sorting, pagination, onTableChange, data, setupMode } = this.props; + const { stats, alerts, sorting, pagination, onTableChange, data, setupMode } = this.props; const columns = this.getColumns(); const flattenedData = data.map((item) => ({ ...item, @@ -176,7 +189,7 @@ export class Listing extends PureComponent { - + {setupModeCallOut} diff --git a/x-pack/plugins/monitoring/public/components/renderers/setup_mode.js b/x-pack/plugins/monitoring/public/components/renderers/setup_mode.js index 5b52f5d85d44d..21e5c1708a05c 100644 --- a/x-pack/plugins/monitoring/public/components/renderers/setup_mode.js +++ b/x-pack/plugins/monitoring/public/components/renderers/setup_mode.js @@ -116,7 +116,7 @@ export class SetupModeRenderer extends React.Component { } getBottomBar(setupModeState) { - if (!setupModeState.enabled) { + if (!setupModeState.enabled || setupModeState.hideBottomBar) { return null; } diff --git a/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js b/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js index 943e100dc5409..8175806cb192a 100644 --- a/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js +++ b/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js @@ -9,6 +9,7 @@ import PropTypes from 'prop-types'; import { isEmpty, capitalize } from 'lodash'; import { EuiFlexGroup, EuiFlexItem, EuiStat } from '@elastic/eui'; import { StatusIcon } from '../status_icon/index.js'; +import { AlertsStatus } from '../../alerts/status'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import './summary_status.scss'; @@ -86,6 +87,7 @@ const StatusIndicator = ({ status, isOnline, IconComponent }) => { export function SummaryStatus({ metrics, status, + alerts, isOnline, IconComponent = DefaultIconComponent, ...props @@ -94,6 +96,19 @@ export function SummaryStatus({
+ {alerts ? ( + + } + titleSize="xxxs" + textAlign="left" + className="monSummaryStatusNoWrap__stat" + description={i18n.translate('xpack.monitoring.summaryStatus.alertsDescription', { + defaultMessage: 'Alerts', + })} + /> + + ) : null} {metrics.map(wrapChild)}
diff --git a/x-pack/plugins/monitoring/public/legacy_shims.ts b/x-pack/plugins/monitoring/public/legacy_shims.ts index 450a34b797c38..0f979e5637d68 100644 --- a/x-pack/plugins/monitoring/public/legacy_shims.ts +++ b/x-pack/plugins/monitoring/public/legacy_shims.ts @@ -4,11 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreStart } from 'kibana/public'; +import { CoreStart, HttpSetup, IUiSettingsClient } from 'kibana/public'; import angular from 'angular'; import { Observable } from 'rxjs'; import { HttpRequestInit } from '../../../../src/core/public'; -import { MonitoringPluginDependencies } from './types'; +import { MonitoringStartPluginDependencies } from './types'; +import { TriggersAndActionsUIPublicPluginSetup } from '../../triggers_actions_ui/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { TypeRegistry } from '../../triggers_actions_ui/public/application/type_registry'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ActionTypeModel, AlertTypeModel } from '../../triggers_actions_ui/public/types'; interface BreadcrumbItem { ['data-test-subj']?: string; @@ -32,7 +37,7 @@ export interface KFetchKibanaOptions { export interface IShims { toastNotifications: CoreStart['notifications']['toasts']; - capabilities: { get: () => CoreStart['application']['capabilities'] }; + capabilities: CoreStart['application']['capabilities']; getAngularInjector: () => angular.auto.IInjectorService; getBasePath: () => string; getInjected: (name: string, defaultValue?: unknown) => unknown; @@ -43,24 +48,29 @@ export interface IShims { I18nContext: CoreStart['i18n']['Context']; docLinks: CoreStart['docLinks']; docTitle: CoreStart['chrome']['docTitle']; - timefilter: MonitoringPluginDependencies['data']['query']['timefilter']['timefilter']; + timefilter: MonitoringStartPluginDependencies['data']['query']['timefilter']['timefilter']; + actionTypeRegistry: TypeRegistry; + alertTypeRegistry: TypeRegistry; + uiSettings: IUiSettingsClient; + http: HttpSetup; kfetch: ( { pathname, ...options }: KFetchOptions, kfetchOptions?: KFetchKibanaOptions | undefined ) => Promise; isCloud: boolean; + triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; } export class Legacy { private static _shims: IShims; public static init( - { core, data, isCloud }: MonitoringPluginDependencies, + { core, data, isCloud, triggersActionsUi }: MonitoringStartPluginDependencies, ngInjector: angular.auto.IInjectorService ) { this._shims = { toastNotifications: core.notifications.toasts, - capabilities: { get: () => core.application.capabilities }, + capabilities: core.application.capabilities, getAngularInjector: (): angular.auto.IInjectorService => ngInjector, getBasePath: (): string => core.http.basePath.get(), getInjected: (name: string, defaultValue?: unknown): string | unknown => @@ -95,6 +105,10 @@ export class Legacy { docLinks: core.docLinks, docTitle: core.chrome.docTitle, timefilter: data.query.timefilter.timefilter, + actionTypeRegistry: triggersActionsUi?.actionTypeRegistry, + alertTypeRegistry: triggersActionsUi?.alertTypeRegistry, + uiSettings: core.uiSettings, + http: core.http, kfetch: async ( { pathname, ...options }: KFetchOptions, kfetchOptions?: KFetchKibanaOptions @@ -104,6 +118,7 @@ export class Legacy { ...options, }), isCloud, + triggersActionsUi, }; } diff --git a/x-pack/plugins/monitoring/public/lib/setup_mode.tsx b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx index 2a4caf17515e1..a36b945e82ef7 100644 --- a/x-pack/plugins/monitoring/public/lib/setup_mode.tsx +++ b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx @@ -39,11 +39,13 @@ interface ISetupModeState { enabled: boolean; data: any; callback?: (() => void) | null; + hideBottomBar: boolean; } const setupModeState: ISetupModeState = { enabled: false, data: null, callback: null, + hideBottomBar: false, }; export const getSetupModeState = () => setupModeState; @@ -128,6 +130,15 @@ export const updateSetupModeData = async (uuid?: string, fetchWithoutClusterUuid } }; +export const hideBottomBar = () => { + setupModeState.hideBottomBar = true; + notifySetupModeDataChange(); +}; +export const showBottomBar = () => { + setupModeState.hideBottomBar = false; + notifySetupModeDataChange(); +}; + export const disableElasticsearchInternalCollection = async () => { checkAngularState(); diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index de8c8d59b78bf..1b9ae75a0968e 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -19,19 +19,25 @@ import { } from '../../../../src/plugins/home/public'; import { UI_SETTINGS } from '../../../../src/plugins/data/public'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; -import { MonitoringPluginDependencies, MonitoringConfig } from './types'; -import { - MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, - KIBANA_ALERTING_ENABLED, -} from '../common/constants'; +import { MonitoringStartPluginDependencies, MonitoringConfig } from './types'; +import { TriggersAndActionsUIPublicPluginSetup } from '../../triggers_actions_ui/public'; +import { createCpuUsageAlertType } from './alerts/cpu_usage_alert'; +import { createLegacyAlertTypes } from './alerts/legacy_alert'; + +interface MonitoringSetupPluginDependencies { + home?: HomePublicPluginSetup; + cloud?: { isCloudEnabled: boolean }; + triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; +} export class MonitoringPlugin - implements Plugin { + implements + Plugin { constructor(private initializerContext: PluginInitializerContext) {} public setup( - core: CoreSetup, - plugins: object & { home?: HomePublicPluginSetup; cloud?: { isCloudEnabled: boolean } } + core: CoreSetup, + plugins: MonitoringSetupPluginDependencies ) { const { home } = plugins; const id = 'monitoring'; @@ -59,6 +65,12 @@ export class MonitoringPlugin }); } + plugins.triggers_actions_ui.alertTypeRegistry.register(createCpuUsageAlertType()); + const legacyAlertTypes = createLegacyAlertTypes(); + for (const legacyAlertType of legacyAlertTypes) { + plugins.triggers_actions_ui.alertTypeRegistry.register(legacyAlertType); + } + const app: App = { id, title, @@ -68,7 +80,7 @@ export class MonitoringPlugin mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart] = await core.getStartServices(); const { AngularApp } = await import('./angular'); - const deps: MonitoringPluginDependencies = { + const deps: MonitoringStartPluginDependencies = { navigation: pluginsStart.navigation, kibanaLegacy: pluginsStart.kibanaLegacy, element: params.element, @@ -77,11 +89,11 @@ export class MonitoringPlugin isCloud: Boolean(plugins.cloud?.isCloudEnabled), pluginInitializerContext: this.initializerContext, externalConfig: this.getExternalConfig(), + triggersActionsUi: plugins.triggers_actions_ui, }; pluginsStart.kibanaLegacy.loadFontAwesome(); this.setInitialTimefilter(deps); - this.overrideAlertingEmailDefaults(deps); const monitoringApp = new AngularApp(deps); const removeHistoryListener = params.history.listen((location) => { @@ -105,7 +117,7 @@ export class MonitoringPlugin public stop() {} - private setInitialTimefilter({ core: coreContext, data }: MonitoringPluginDependencies) { + private setInitialTimefilter({ core: coreContext, data }: MonitoringStartPluginDependencies) { const { timefilter } = data.query.timefilter; const { uiSettings } = coreContext; const refreshInterval = { value: 10000, pause: false }; @@ -119,25 +131,6 @@ export class MonitoringPlugin uiSettings.overrideLocalDefault('timepicker:timeDefaults', JSON.stringify(time)); } - private overrideAlertingEmailDefaults({ core: coreContext }: MonitoringPluginDependencies) { - const { uiSettings } = coreContext; - if (KIBANA_ALERTING_ENABLED && !uiSettings.get(MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS)) { - uiSettings.overrideLocalDefault( - MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, - JSON.stringify({ - name: i18n.translate('xpack.monitoring.alertingEmailAddress.name', { - defaultMessage: 'Alerting email address', - }), - value: '', - description: i18n.translate('xpack.monitoring.alertingEmailAddress.description', { - defaultMessage: `The default email address to receive alerts from Stack Monitoring`, - }), - category: ['monitoring'], - }) - ); - } - } - private getExternalConfig() { const monitoring = this.initializerContext.config.get(); return [ diff --git a/x-pack/plugins/monitoring/public/services/clusters.js b/x-pack/plugins/monitoring/public/services/clusters.js index 2862c6f424927..f3eadcaf9831b 100644 --- a/x-pack/plugins/monitoring/public/services/clusters.js +++ b/x-pack/plugins/monitoring/public/services/clusters.js @@ -19,6 +19,8 @@ function formatCluster(cluster) { return cluster; } +let once = false; + export function monitoringClustersProvider($injector) { return (clusterUuid, ccs, codePaths) => { const { min, max } = Legacy.shims.timefilter.getBounds(); @@ -30,23 +32,52 @@ export function monitoringClustersProvider($injector) { } const $http = $injector.get('$http'); - return $http - .post(url, { - ccs, - timeRange: { - min: min.toISOString(), - max: max.toISOString(), - }, - codePaths, - }) - .then((response) => response.data) - .then((data) => { - return formatClusters(data); // return set of clusters - }) - .catch((err) => { + + function getClusters() { + return $http + .post(url, { + ccs, + timeRange: { + min: min.toISOString(), + max: max.toISOString(), + }, + codePaths, + }) + .then((response) => response.data) + .then((data) => { + return formatClusters(data); // return set of clusters + }) + .catch((err) => { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); + } + + function ensureAlertsEnabled() { + return $http.post('../api/monitoring/v1/alerts/enable', {}).catch((err) => { const Private = $injector.get('Private'); const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); return ajaxErrorHandlers(err); }); + } + + if (!once) { + return getClusters().then((clusters) => { + if (clusters.length) { + return ensureAlertsEnabled() + .then(() => { + once = true; + return clusters; + }) + .catch(() => { + // Intentionally swallow the error as this will retry the next page load + return clusters; + }); + } + return clusters; + }); + } + return getClusters(); }; } diff --git a/x-pack/plugins/monitoring/public/types.ts b/x-pack/plugins/monitoring/public/types.ts index 6266755a04120..f911af2db8c58 100644 --- a/x-pack/plugins/monitoring/public/types.ts +++ b/x-pack/plugins/monitoring/public/types.ts @@ -7,12 +7,13 @@ import { PluginInitializerContext, CoreStart } from 'kibana/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../src/plugins/navigation/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import { TriggersAndActionsUIPublicPluginSetup } from '../../triggers_actions_ui/public'; import { KibanaLegacyStart } from '../../../../src/plugins/kibana_legacy/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths export { MonitoringConfig } from '../server'; -export interface MonitoringPluginDependencies { +export interface MonitoringStartPluginDependencies { navigation: NavigationStart; data: DataPublicPluginStart; kibanaLegacy: KibanaLegacyStart; @@ -21,4 +22,5 @@ export interface MonitoringPluginDependencies { isCloud: boolean; pluginInitializerContext: PluginInitializerContext; externalConfig: Array | Array>; + triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; } diff --git a/x-pack/plugins/monitoring/public/url_state.ts b/x-pack/plugins/monitoring/public/url_state.ts index f2ae0a93d5df0..e53497d751f9b 100644 --- a/x-pack/plugins/monitoring/public/url_state.ts +++ b/x-pack/plugins/monitoring/public/url_state.ts @@ -6,7 +6,7 @@ import { Subscription } from 'rxjs'; import { History, createHashHistory } from 'history'; -import { MonitoringPluginDependencies } from './types'; +import { MonitoringStartPluginDependencies } from './types'; import { Legacy } from './legacy_shims'; import { @@ -64,13 +64,13 @@ export class GlobalState { private readonly stateStorage: IKbnUrlStateStorage; private readonly stateContainerChangeSub: Subscription; private readonly syncQueryStateWithUrlManager: { stop: () => void }; - private readonly timefilterRef: MonitoringPluginDependencies['data']['query']['timefilter']['timefilter']; + private readonly timefilterRef: MonitoringStartPluginDependencies['data']['query']['timefilter']['timefilter']; private lastAssignedState: MonitoringAppState = {}; private lastKnownGlobalState?: string; constructor( - queryService: MonitoringPluginDependencies['data']['query'], + queryService: MonitoringStartPluginDependencies['data']['query'], rootScope: ng.IRootScopeService, ngLocation: ng.ILocationService, externalState: RawObject diff --git a/x-pack/plugins/monitoring/public/views/alerts/index.html b/x-pack/plugins/monitoring/public/views/alerts/index.html deleted file mode 100644 index 4a764634d86fa..0000000000000 --- a/x-pack/plugins/monitoring/public/views/alerts/index.html +++ /dev/null @@ -1,3 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/alerts/index.js b/x-pack/plugins/monitoring/public/views/alerts/index.js deleted file mode 100644 index ea857cb69d22b..0000000000000 --- a/x-pack/plugins/monitoring/public/views/alerts/index.js +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { render } from 'react-dom'; -import { find, get } from 'lodash'; -import { uiRoutes } from '../../angular/helpers/routes'; -import template from './index.html'; -import { routeInitProvider } from '../../lib/route_init'; -import { ajaxErrorHandlersProvider } from '../../lib/ajax_error_handler'; -import { Legacy } from '../../legacy_shims'; -import { Alerts } from '../../components/alerts'; -import { MonitoringViewBaseEuiTableController } from '../base_eui_table_controller'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPage, EuiPageBody, EuiPageContent, EuiSpacer, EuiLink } from '@elastic/eui'; -import { CODE_PATH_ALERTS, KIBANA_ALERTING_ENABLED } from '../../../common/constants'; - -function getPageData($injector) { - const globalState = $injector.get('globalState'); - const $http = $injector.get('$http'); - const Private = $injector.get('Private'); - const url = KIBANA_ALERTING_ENABLED - ? `../api/monitoring/v1/alert_status` - : `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/legacy_alerts`; - - const timeBounds = Legacy.shims.timefilter.getBounds(); - const data = { - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }; - - if (!KIBANA_ALERTING_ENABLED) { - data.ccs = globalState.ccs; - } - - return $http - .post(url, data) - .then((response) => { - const result = get(response, 'data', []); - if (KIBANA_ALERTING_ENABLED) { - return result.alerts; - } - return result; - }) - .catch((err) => { - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} - -uiRoutes.when('/alerts', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_ALERTS] }); - }, - alerts: getPageData, - }, - controllerAs: 'alerts', - controller: class AlertsView extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope) { - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - - // breadcrumbs + page title - $scope.cluster = find($route.current.locals.clusters, { - cluster_uuid: globalState.cluster_uuid, - }); - - super({ - title: i18n.translate('xpack.monitoring.alerts.clusterAlertsTitle', { - defaultMessage: 'Cluster Alerts', - }), - getPageData, - $scope, - $injector, - storageKey: 'alertsTable', - reactNodeId: 'monitoringAlertsApp', - }); - - this.data = $route.current.locals.alerts; - - const renderReact = (data) => { - const app = data.message ? ( -

{data.message}

- ) : ( - - ); - - render( - - - - {app} - - - - - - - , - document.getElementById('monitoringAlertsApp') - ); - }; - $scope.$watch( - () => this.data, - (data) => renderReact(data) - ); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/all.js b/x-pack/plugins/monitoring/public/views/all.js index 51dcce751863c..d192b366fec33 100644 --- a/x-pack/plugins/monitoring/public/views/all.js +++ b/x-pack/plugins/monitoring/public/views/all.js @@ -6,7 +6,6 @@ import './no_data'; import './access_denied'; -import './alerts'; import './license'; import './cluster/listing'; import './cluster/overview'; diff --git a/x-pack/plugins/monitoring/public/views/base_controller.js b/x-pack/plugins/monitoring/public/views/base_controller.js index e189491a3be03..2f88245d88c4a 100644 --- a/x-pack/plugins/monitoring/public/views/base_controller.js +++ b/x-pack/plugins/monitoring/public/views/base_controller.js @@ -85,6 +85,7 @@ export class MonitoringViewBaseController { $scope, $injector, options = {}, + alerts = { shouldFetch: false, options: {} }, fetchDataImmediately = true, }) { const titleService = $injector.get('title'); @@ -112,6 +113,34 @@ export class MonitoringViewBaseController { const { enableTimeFilter = true, enableAutoRefresh = true } = options; + async function fetchAlerts() { + const globalState = $injector.get('globalState'); + const bounds = Legacy.shims.timefilter.getBounds(); + const min = bounds.min?.valueOf(); + const max = bounds.max?.valueOf(); + const options = alerts.options || {}; + try { + return await Legacy.shims.http.post( + `/api/monitoring/v1/alert/${globalState.cluster_uuid}/status`, + { + body: JSON.stringify({ + alertTypeIds: options.alertTypeIds, + filters: options.filters, + timeRange: { + min, + max, + }, + }), + } + ); + } catch (err) { + Legacy.shims.toastNotifications.addDanger({ + title: 'Error fetching alert status', + text: err.message, + }); + } + } + this.updateData = () => { if (this.updateDataPromise) { // Do not sent another request if one is inflight @@ -122,14 +151,18 @@ export class MonitoringViewBaseController { const _api = apiUrlFn ? apiUrlFn() : api; const promises = [_getPageData($injector, _api, this.getPaginationRouteOptions())]; const setupMode = getSetupModeState(); + if (alerts.shouldFetch) { + promises.push(fetchAlerts()); + } if (setupMode.enabled) { promises.push(updateSetupModeData()); } this.updateDataPromise = new PromiseWithCancel(Promise.all(promises)); - return this.updateDataPromise.promise().then(([pageData]) => { + return this.updateDataPromise.promise().then(([pageData, alerts]) => { $scope.$apply(() => { this._isDataInitialized = true; // render will replace loading screen with the react component $scope.pageData = this.data = pageData; // update the view's data with the fetch result + $scope.alerts = this.alerts = alerts; }); }); }; diff --git a/x-pack/plugins/monitoring/public/views/cluster/overview/index.js b/x-pack/plugins/monitoring/public/views/cluster/overview/index.js index d47b31cfb5b79..f3e6d5def9b6f 100644 --- a/x-pack/plugins/monitoring/public/views/cluster/overview/index.js +++ b/x-pack/plugins/monitoring/public/views/cluster/overview/index.js @@ -5,7 +5,6 @@ */ import React, { Fragment } from 'react'; import { isEmpty } from 'lodash'; -import { Legacy } from '../../../legacy_shims'; import { i18n } from '@kbn/i18n'; import { uiRoutes } from '../../../angular/helpers/routes'; import { routeInitProvider } from '../../../lib/route_init'; @@ -13,11 +12,7 @@ import template from './index.html'; import { MonitoringViewBaseController } from '../../'; import { Overview } from '../../../components/cluster/overview'; import { SetupModeRenderer } from '../../../components/renderers'; -import { - CODE_PATH_ALL, - MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, - KIBANA_ALERTING_ENABLED, -} from '../../../../common/constants'; +import { CODE_PATH_ALL } from '../../../../common/constants'; const CODE_PATHS = [CODE_PATH_ALL]; @@ -35,7 +30,6 @@ uiRoutes.when('/overview', { const monitoringClusters = $injector.get('monitoringClusters'); const globalState = $injector.get('globalState'); const showLicenseExpiration = $injector.get('showLicenseExpiration'); - const config = $injector.get('config'); super({ title: i18n.translate('xpack.monitoring.cluster.overviewTitle', { @@ -53,6 +47,9 @@ uiRoutes.when('/overview', { reactNodeId: 'monitoringClusterOverviewApp', $scope, $injector, + alerts: { + shouldFetch: true, + }, }); $scope.$watch( @@ -62,11 +59,6 @@ uiRoutes.when('/overview', { return; } - let emailAddress = Legacy.shims.getInjected('monitoringLegacyEmailAddress') || ''; - if (KIBANA_ALERTING_ENABLED) { - emailAddress = config.get(MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS) || emailAddress; - } - this.renderReact( diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js index a1ce9bda16cdc..f6f7a01690529 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js @@ -18,7 +18,7 @@ import { Node } from '../../../components/elasticsearch/node/node'; import { labels } from '../../../components/elasticsearch/shard_allocation/lib/labels'; import { nodesByIndices } from '../../../components/elasticsearch/shard_allocation/transformers/nodes_by_indices'; import { MonitoringViewBaseController } from '../../base_controller'; -import { CODE_PATH_ELASTICSEARCH } from '../../../../common/constants'; +import { CODE_PATH_ELASTICSEARCH, ALERT_CPU_USAGE } from '../../../../common/constants'; uiRoutes.when('/elasticsearch/nodes/:node', { template, @@ -47,6 +47,17 @@ uiRoutes.when('/elasticsearch/nodes/:node', { reactNodeId: 'monitoringElasticsearchNodeApp', $scope, $injector, + alerts: { + shouldFetch: true, + options: { + alertTypeIds: [ALERT_CPU_USAGE], + filters: [ + { + nodeUuid: nodeName, + }, + ], + }, + }, }); this.nodeName = nodeName; @@ -79,6 +90,7 @@ uiRoutes.when('/elasticsearch/nodes/:node', { this.renderReact( diff --git a/x-pack/plugins/monitoring/public/views/kibana/instance/index.js b/x-pack/plugins/monitoring/public/views/kibana/instance/index.js index 802c0e3d30d5b..a7cb6c8094f74 100644 --- a/x-pack/plugins/monitoring/public/views/kibana/instance/index.js +++ b/x-pack/plugins/monitoring/public/views/kibana/instance/index.js @@ -26,7 +26,8 @@ import { import { MonitoringTimeseriesContainer } from '../../../components/chart'; import { DetailStatus } from '../../../components/kibana/detail_status'; import { MonitoringViewBaseController } from '../../base_controller'; -import { CODE_PATH_KIBANA } from '../../../../common/constants'; +import { CODE_PATH_KIBANA, ALERT_KIBANA_VERSION_MISMATCH } from '../../../../common/constants'; +import { AlertsCallout } from '../../../alerts/callout'; function getPageData($injector) { const $http = $injector.get('$http'); @@ -70,6 +71,12 @@ uiRoutes.when('/kibana/instances/:uuid', { reactNodeId: 'monitoringKibanaInstanceApp', $scope, $injector, + alerts: { + shouldFetch: true, + options: { + alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH], + }, + }, }); $scope.$watch( @@ -88,6 +95,7 @@ uiRoutes.when('/kibana/instances/:uuid', { + diff --git a/x-pack/plugins/monitoring/public/views/kibana/instances/index.js b/x-pack/plugins/monitoring/public/views/kibana/instances/index.js index 8556103e47c30..7106da0fdabd3 100644 --- a/x-pack/plugins/monitoring/public/views/kibana/instances/index.js +++ b/x-pack/plugins/monitoring/public/views/kibana/instances/index.js @@ -12,7 +12,11 @@ import { getPageData } from './get_page_data'; import template from './index.html'; import { KibanaInstances } from '../../../components/kibana/instances'; import { SetupModeRenderer } from '../../../components/renderers'; -import { KIBANA_SYSTEM_ID, CODE_PATH_KIBANA } from '../../../../common/constants'; +import { + KIBANA_SYSTEM_ID, + CODE_PATH_KIBANA, + ALERT_KIBANA_VERSION_MISMATCH, +} from '../../../../common/constants'; uiRoutes.when('/kibana/instances', { template, @@ -33,6 +37,12 @@ uiRoutes.when('/kibana/instances', { reactNodeId: 'monitoringKibanaInstancesApp', $scope, $injector, + alerts: { + shouldFetch: true, + options: { + alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH], + }, + }, }); const renderReact = () => { @@ -46,6 +56,7 @@ uiRoutes.when('/kibana/instances', { {flyoutComponent} + {metricsToShow.map((metric, index) => ( diff --git a/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js b/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js index f78a426b9b7c3..563d04af55bb2 100644 --- a/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js +++ b/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js @@ -11,7 +11,11 @@ import { getPageData } from './get_page_data'; import template from './index.html'; import { Listing } from '../../../components/logstash/listing'; import { SetupModeRenderer } from '../../../components/renderers'; -import { CODE_PATH_LOGSTASH, LOGSTASH_SYSTEM_ID } from '../../../../common/constants'; +import { + CODE_PATH_LOGSTASH, + LOGSTASH_SYSTEM_ID, + ALERT_LOGSTASH_VERSION_MISMATCH, +} from '../../../../common/constants'; uiRoutes.when('/logstash/nodes', { template, @@ -32,6 +36,12 @@ uiRoutes.when('/logstash/nodes', { reactNodeId: 'monitoringLogstashNodesApp', $scope, $injector, + alerts: { + shouldFetch: true, + options: { + alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH], + }, + }, }); $scope.$watch( @@ -49,6 +59,7 @@ uiRoutes.when('/logstash/nodes', { data={data.nodes} setupMode={setupMode} stats={data.clusterStatus} + alerts={this.alerts} sorting={this.sorting} pagination={this.pagination} onTableChange={this.onTableChange} diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts b/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts new file mode 100644 index 0000000000000..d8fa703c7f785 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AlertsFactory } from './alerts_factory'; +import { ALERT_CPU_USAGE } from '../../common/constants'; + +describe('AlertsFactory', () => { + const alertsClient = { + find: jest.fn(), + }; + + afterEach(() => { + alertsClient.find.mockReset(); + }); + + it('should get by type', async () => { + const id = '1abc'; + alertsClient.find = jest.fn().mockImplementation(() => { + return { + total: 1, + data: [ + { + id, + }, + ], + }; + }); + const alert = await AlertsFactory.getByType(ALERT_CPU_USAGE, alertsClient as any); + expect(alert).not.toBeNull(); + expect(alert?.type).toBe(ALERT_CPU_USAGE); + }); + + it('should handle no alert found', async () => { + alertsClient.find = jest.fn().mockImplementation(() => { + return { + total: 0, + }; + }); + const alert = await AlertsFactory.getByType(ALERT_CPU_USAGE, alertsClient as any); + expect(alert).not.toBeNull(); + expect(alert?.type).toBe(ALERT_CPU_USAGE); + }); + + it('should pass in the correct filters', async () => { + let filter = null; + alertsClient.find = jest.fn().mockImplementation(({ options }) => { + filter = options.filter; + return { + total: 0, + }; + }); + await AlertsFactory.getByType(ALERT_CPU_USAGE, alertsClient as any); + expect(filter).toBe(`alert.attributes.alertTypeId:${ALERT_CPU_USAGE}`); + }); + + it('should handle no alerts client', async () => { + const alert = await AlertsFactory.getByType(ALERT_CPU_USAGE, undefined); + expect(alert).not.toBeNull(); + expect(alert?.type).toBe(ALERT_CPU_USAGE); + }); + + it('should get all', () => { + const alerts = AlertsFactory.getAll(); + expect(alerts.length).toBe(7); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts b/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts new file mode 100644 index 0000000000000..b91eab05cf912 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + CpuUsageAlert, + NodesChangedAlert, + ClusterHealthAlert, + LicenseExpirationAlert, + LogstashVersionMismatchAlert, + KibanaVersionMismatchAlert, + ElasticsearchVersionMismatchAlert, + BaseAlert, +} from './'; +import { + ALERT_CLUSTER_HEALTH, + ALERT_LICENSE_EXPIRATION, + ALERT_CPU_USAGE, + ALERT_NODES_CHANGED, + ALERT_LOGSTASH_VERSION_MISMATCH, + ALERT_KIBANA_VERSION_MISMATCH, + ALERT_ELASTICSEARCH_VERSION_MISMATCH, +} from '../../common/constants'; +import { AlertsClient } from '../../../alerts/server'; + +export const BY_TYPE = { + [ALERT_CLUSTER_HEALTH]: ClusterHealthAlert, + [ALERT_LICENSE_EXPIRATION]: LicenseExpirationAlert, + [ALERT_CPU_USAGE]: CpuUsageAlert, + [ALERT_NODES_CHANGED]: NodesChangedAlert, + [ALERT_LOGSTASH_VERSION_MISMATCH]: LogstashVersionMismatchAlert, + [ALERT_KIBANA_VERSION_MISMATCH]: KibanaVersionMismatchAlert, + [ALERT_ELASTICSEARCH_VERSION_MISMATCH]: ElasticsearchVersionMismatchAlert, +}; + +export class AlertsFactory { + public static async getByType( + type: string, + alertsClient: AlertsClient | undefined + ): Promise { + const alertCls = BY_TYPE[type]; + if (!alertCls) { + return null; + } + if (alertsClient) { + const alertClientAlerts = await alertsClient.find({ + options: { + filter: `alert.attributes.alertTypeId:${type}`, + }, + }); + + if (alertClientAlerts.total === 0) { + return new alertCls(); + } + + const rawAlert = alertClientAlerts.data[0]; + return new alertCls(rawAlert as BaseAlert['rawAlert']); + } + + return new alertCls(); + } + + public static getAll() { + return Object.values(BY_TYPE).map((alert) => new alert()); + } +} diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.test.ts new file mode 100644 index 0000000000000..8fd31db421a30 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.test.ts @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { BaseAlert } from './base_alert'; + +describe('BaseAlert', () => { + describe('serialize', () => { + it('should serialize with a raw alert provided', () => { + const alert = new BaseAlert({} as any); + expect(alert.serialize()).not.toBeNull(); + }); + it('should not serialize without a raw alert provided', () => { + const alert = new BaseAlert(); + expect(alert.serialize()).toBeNull(); + }); + }); + + describe('create', () => { + it('should create an alert if it does not exist', async () => { + const alert = new BaseAlert(); + const alertsClient = { + create: jest.fn(), + find: jest.fn().mockImplementation(() => { + return { + total: 0, + }; + }), + }; + const actionsClient = { + get: jest.fn().mockImplementation(() => { + return { + actionTypeId: 'foo', + }; + }), + }; + const actions = [ + { + id: '1abc', + config: {}, + }, + ]; + + await alert.createIfDoesNotExist(alertsClient as any, actionsClient as any, actions); + expect(alertsClient.create).toHaveBeenCalledWith({ + data: { + actions: [ + { + group: 'default', + id: '1abc', + params: { + message: '{{context.internalShortMessage}}', + }, + }, + ], + alertTypeId: undefined, + consumer: 'monitoring', + enabled: true, + name: undefined, + params: {}, + schedule: { + interval: '1m', + }, + tags: [], + throttle: '1m', + }, + }); + }); + + it('should not create an alert if it exists', async () => { + const alert = new BaseAlert(); + const alertsClient = { + create: jest.fn(), + find: jest.fn().mockImplementation(() => { + return { + total: 1, + data: [], + }; + }), + }; + const actionsClient = { + get: jest.fn().mockImplementation(() => { + return { + actionTypeId: 'foo', + }; + }), + }; + const actions = [ + { + id: '1abc', + config: {}, + }, + ]; + + await alert.createIfDoesNotExist(alertsClient as any, actionsClient as any, actions); + expect(alertsClient.create).not.toHaveBeenCalled(); + }); + }); + + describe('getStates', () => { + it('should get alert states', async () => { + const alertsClient = { + getAlertState: jest.fn().mockImplementation(() => { + return { + alertInstances: { + abc123: { + id: 'foobar', + }, + }, + }; + }), + }; + const id = '456def'; + const filters: any[] = []; + const alert = new BaseAlert(); + const states = await alert.getStates(alertsClient as any, id, filters); + expect(states).toStrictEqual({ + abc123: { + id: 'foobar', + }, + }); + }); + + it('should return nothing if no states are available', async () => { + const alertsClient = { + getAlertState: jest.fn().mockImplementation(() => { + return null; + }), + }; + const id = '456def'; + const filters: any[] = []; + const alert = new BaseAlert(); + const states = await alert.getStates(alertsClient as any, id, filters); + expect(states).toStrictEqual({}); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts new file mode 100644 index 0000000000000..622ee7dc51af1 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -0,0 +1,339 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + UiSettingsServiceStart, + ILegacyCustomClusterClient, + Logger, + IUiSettingsClient, +} from 'kibana/server'; +import { i18n } from '@kbn/i18n'; +import { + AlertType, + AlertExecutorOptions, + AlertInstance, + AlertsClient, + AlertServices, +} from '../../../alerts/server'; +import { Alert, RawAlertInstance } from '../../../alerts/common'; +import { ActionsClient } from '../../../actions/server'; +import { + AlertState, + AlertCluster, + AlertMessage, + AlertData, + AlertInstanceState, + AlertEnableAction, +} from './types'; +import { fetchAvailableCcs } from '../lib/alerts/fetch_available_ccs'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; +import { MonitoringConfig } from '../config'; +import { AlertSeverity } from '../../common/enums'; +import { CommonAlertFilter, CommonAlertParams, CommonBaseAlert } from '../../common/types'; +import { MonitoringLicenseService } from '../types'; + +export class BaseAlert { + public type!: string; + public label!: string; + public defaultThrottle: string = '1m'; + public defaultInterval: string = '1m'; + public rawAlert: Alert | undefined; + public isLegacy: boolean = false; + + protected getUiSettingsService!: () => Promise; + protected monitoringCluster!: ILegacyCustomClusterClient; + protected getLogger!: (...scopes: string[]) => Logger; + protected config!: MonitoringConfig; + protected kibanaUrl!: string; + protected defaultParams: CommonAlertParams | {} = {}; + public get paramDetails() { + return {}; + } + protected actionVariables: Array<{ name: string; description: string }> = []; + protected alertType!: AlertType; + + constructor(rawAlert: Alert | undefined = undefined) { + if (rawAlert) { + this.rawAlert = rawAlert; + } + } + + public serialize(): CommonBaseAlert | null { + if (!this.rawAlert) { + return null; + } + + return { + type: this.type, + label: this.label, + rawAlert: this.rawAlert, + paramDetails: this.paramDetails, + isLegacy: this.isLegacy, + }; + } + + public initializeAlertType( + getUiSettingsService: () => Promise, + monitoringCluster: ILegacyCustomClusterClient, + getLogger: (...scopes: string[]) => Logger, + config: MonitoringConfig, + kibanaUrl: string + ) { + this.getUiSettingsService = getUiSettingsService; + this.monitoringCluster = monitoringCluster; + this.config = config; + this.kibanaUrl = kibanaUrl; + this.getLogger = getLogger; + } + + public getAlertType(): AlertType { + return { + id: this.type, + name: this.label, + actionGroups: [ + { + id: 'default', + name: i18n.translate('xpack.monitoring.alerts.actionGroups.default', { + defaultMessage: 'Default', + }), + }, + ], + defaultActionGroupId: 'default', + executor: (options: AlertExecutorOptions): Promise => this.execute(options), + producer: 'monitoring', + actionVariables: { + context: this.actionVariables, + }, + }; + } + + public isEnabled(licenseService: MonitoringLicenseService) { + if (this.isLegacy) { + const watcherFeature = licenseService.getWatcherFeature(); + if (!watcherFeature.isAvailable || !watcherFeature.isEnabled) { + return false; + } + } + return true; + } + + public getId() { + return this.rawAlert ? this.rawAlert.id : null; + } + + public async createIfDoesNotExist( + alertsClient: AlertsClient, + actionsClient: ActionsClient, + actions: AlertEnableAction[] + ): Promise { + const existingAlertData = await alertsClient.find({ + options: { + search: this.type, + }, + }); + + if (existingAlertData.total > 0) { + const existingAlert = existingAlertData.data[0] as Alert; + return existingAlert; + } + + const alertActions = []; + for (const actionData of actions) { + const action = await actionsClient.get({ id: actionData.id }); + if (!action) { + continue; + } + alertActions.push({ + group: 'default', + id: actionData.id, + params: { + // This is just a server log right now, but will get more robut over time + message: this.getDefaultActionMessage(true), + ...actionData.config, + }, + }); + } + + return await alertsClient.create({ + data: { + enabled: true, + tags: [], + params: this.defaultParams, + consumer: 'monitoring', + name: this.label, + alertTypeId: this.type, + throttle: this.defaultThrottle, + schedule: { interval: this.defaultInterval }, + actions: alertActions, + }, + }); + } + + public async getStates( + alertsClient: AlertsClient, + id: string, + filters: CommonAlertFilter[] + ): Promise<{ [instanceId: string]: RawAlertInstance }> { + const states = await alertsClient.getAlertState({ id }); + if (!states || !states.alertInstances) { + return {}; + } + + return Object.keys(states.alertInstances).reduce( + (accum: { [instanceId: string]: RawAlertInstance }, instanceId) => { + if (!states.alertInstances) { + return accum; + } + const alertInstance: RawAlertInstance = states.alertInstances[instanceId]; + if (alertInstance && this.filterAlertInstance(alertInstance, filters)) { + accum[instanceId] = alertInstance; + } + return accum; + }, + {} + ); + } + + protected filterAlertInstance(alertInstance: RawAlertInstance, filters: CommonAlertFilter[]) { + return true; + } + + protected async execute({ services, params, state }: AlertExecutorOptions): Promise { + const logger = this.getLogger(this.type); + logger.debug( + `Executing alert with params: ${JSON.stringify(params)} and state: ${JSON.stringify(state)}` + ); + + const callCluster = this.monitoringCluster + ? this.monitoringCluster.callAsInternalUser + : services.callCluster; + const availableCcs = this.config.ui.ccs.enabled ? await fetchAvailableCcs(callCluster) : []; + // Support CCS use cases by querying to find available remote clusters + // and then adding those to the index pattern we are searching against + let esIndexPattern = INDEX_PATTERN_ELASTICSEARCH; + if (availableCcs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + const clusters = await fetchClusters(callCluster, esIndexPattern); + const uiSettings = (await this.getUiSettingsService()).asScopedToClient( + services.savedObjectsClient + ); + + const data = await this.fetchData(params, callCluster, clusters, uiSettings, availableCcs); + this.processData(data, clusters, services, logger); + } + + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + uiSettings: IUiSettingsClient, + availableCcs: string[] + ): Promise { + // Child should implement + throw new Error('Child classes must implement `fetchData`'); + } + + protected processData( + data: AlertData[], + clusters: AlertCluster[], + services: AlertServices, + logger: Logger + ) { + for (const item of data) { + const cluster = clusters.find((c: AlertCluster) => c.clusterUuid === item.clusterUuid); + if (!cluster) { + logger.warn(`Unable to find cluster for clusterUuid='${item.clusterUuid}'`); + continue; + } + + const instance = services.alertInstanceFactory(`${this.type}:${item.instanceKey}`); + const state = (instance.getState() as unknown) as AlertInstanceState; + const alertInstanceState: AlertInstanceState = { alertStates: state?.alertStates || [] }; + let alertState: AlertState; + const indexInState = this.findIndexInInstanceState(alertInstanceState, cluster); + if (indexInState > -1) { + alertState = state.alertStates[indexInState]; + } else { + alertState = this.getDefaultAlertState(cluster, item); + } + + let shouldExecuteActions = false; + if (item.shouldFire) { + logger.debug(`${this.type} is firing`); + alertState.ui.triggeredMS = +new Date(); + alertState.ui.isFiring = true; + alertState.ui.message = this.getUiMessage(alertState, item); + alertState.ui.severity = item.severity; + alertState.ui.resolvedMS = 0; + shouldExecuteActions = true; + } else if (!item.shouldFire && alertState.ui.isFiring) { + logger.debug(`${this.type} is not firing anymore`); + alertState.ui.isFiring = false; + alertState.ui.resolvedMS = +new Date(); + alertState.ui.message = this.getUiMessage(alertState, item); + shouldExecuteActions = true; + } + + if (indexInState === -1) { + alertInstanceState.alertStates.push(alertState); + } else { + alertInstanceState.alertStates = [ + ...alertInstanceState.alertStates.slice(0, indexInState), + alertState, + ...alertInstanceState.alertStates.slice(indexInState + 1), + ]; + } + + instance.replaceState(alertInstanceState); + if (shouldExecuteActions) { + this.executeActions(instance, alertInstanceState, item, cluster); + } + } + } + + public getDefaultActionMessage(forDefaultServerLog: boolean): string { + return forDefaultServerLog + ? '{{context.internalShortMessage}}' + : '{{context.internalFullMessage}}'; + } + + protected findIndexInInstanceState(stateInstance: AlertInstanceState, cluster: AlertCluster) { + return stateInstance.alertStates.findIndex( + (alertState) => alertState.cluster.clusterUuid === cluster.clusterUuid + ); + } + + protected getDefaultAlertState(cluster: AlertCluster, item: AlertData): AlertState { + return { + cluster, + ccs: item.ccs, + ui: { + isFiring: false, + message: null, + severity: AlertSeverity.Success, + resolvedMS: 0, + triggeredMS: 0, + lastCheckedMS: 0, + }, + }; + } + + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { + throw new Error('Child classes must implement `getUiMessage`'); + } + + protected executeActions( + instance: AlertInstance, + instanceState: AlertInstanceState, + item: AlertData, + cluster: AlertCluster + ) { + throw new Error('Child classes must implement `executeActions`'); + } +} diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts new file mode 100644 index 0000000000000..10b75c43ac879 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts @@ -0,0 +1,261 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ClusterHealthAlert } from './cluster_health_alert'; +import { ALERT_CLUSTER_HEALTH } from '../../common/constants'; +import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; + +const RealDate = Date; + +jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ + fetchLegacyAlerts: jest.fn(), +})); +jest.mock('../lib/alerts/fetch_clusters', () => ({ + fetchClusters: jest.fn(), +})); + +describe('ClusterHealthAlert', () => { + it('should have defaults', () => { + const alert = new ClusterHealthAlert(); + expect(alert.type).toBe(ALERT_CLUSTER_HEALTH); + expect(alert.label).toBe('Cluster health'); + expect(alert.defaultThrottle).toBe('1m'); + // @ts-ignore + expect(alert.actionVariables).toStrictEqual([ + { + name: 'internalShortMessage', + description: 'The short internal message generated by Elastic.', + }, + { + name: 'internalFullMessage', + description: 'The full internal message generated by Elastic.', + }, + { name: 'state', description: 'The current state of the alert.' }, + { name: 'clusterHealth', description: 'The health of the cluster.' }, + { name: 'clusterName', description: 'The cluster to which the nodes belong.' }, + { name: 'action', description: 'The recommended action for this alert.' }, + { + name: 'actionPlain', + description: 'The recommended action for this alert, without any markdown.', + }, + ]); + }); + + describe('execute', () => { + function FakeDate() {} + FakeDate.prototype.valueOf = () => 1; + + const clusterUuid = 'abc123'; + const clusterName = 'testCluster'; + const legacyAlert = { + prefix: 'Elasticsearch cluster status is yellow.', + message: 'Allocate missing replica shards.', + metadata: { + severity: 2000, + cluster_uuid: clusterUuid, + }, + }; + const getUiSettingsService = () => ({ + asScopedToClient: jest.fn(), + }); + const getLogger = () => ({ + debug: jest.fn(), + }); + const monitoringCluster = null; + const config = { + ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + }; + const kibanaUrl = 'http://localhost:5601'; + + const replaceState = jest.fn(); + const scheduleActions = jest.fn(); + const getState = jest.fn(); + const executorOptions = { + services: { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, + state: {}, + }; + + beforeEach(() => { + // @ts-ignore + Date = FakeDate; + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return [legacyAlert]; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + replaceState.mockReset(); + scheduleActions.mockReset(); + getState.mockReset(); + }); + + it('should fire actions', async () => { + const alert = new ClusterHealthAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, + ccs: null, + ui: { + isFiring: true, + message: { + text: 'Elasticsearch cluster health is yellow.', + nextSteps: [ + { + text: 'Allocate missing replica shards. #start_linkView now#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'link', + url: 'elasticsearch/indices', + }, + ], + }, + ], + }, + severity: 'danger', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + action: + '[Allocate missing replica shards.](http://localhost:5601/app/monitoring#elasticsearch/indices?_g=(cluster_uuid:abc123))', + actionPlain: 'Allocate missing replica shards.', + internalFullMessage: + 'Cluster health alert is firing for testCluster. Current health is yellow. [Allocate missing replica shards.](http://localhost:5601/app/monitoring#elasticsearch/indices?_g=(cluster_uuid:abc123))', + internalShortMessage: + 'Cluster health alert is firing for testCluster. Current health is yellow. Allocate missing replica shards.', + clusterName, + clusterHealth: 'yellow', + state: 'firing', + }); + }); + + it('should not fire actions if there is no legacy alert', async () => { + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return []; + }); + const alert = new ClusterHealthAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).not.toHaveBeenCalledWith({}); + expect(scheduleActions).not.toHaveBeenCalled(); + }); + + it('should resolve with a resolved message', async () => { + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return [ + { + ...legacyAlert, + resolved_timestamp: 1, + }, + ]; + }); + (getState as jest.Mock).mockImplementation(() => { + return { + alertStates: [ + { + cluster: { + clusterUuid, + clusterName, + }, + ccs: null, + ui: { + isFiring: true, + message: null, + severity: 'danger', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }; + }); + const alert = new ClusterHealthAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid, clusterName }, + ccs: null, + ui: { + isFiring: false, + message: { + text: 'Elasticsearch cluster health is green.', + }, + severity: 'danger', + resolvedMS: 1, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: 'Cluster health alert is resolved for testCluster.', + internalShortMessage: 'Cluster health alert is resolved for testCluster.', + clusterName, + clusterHealth: 'yellow', + state: 'resolved', + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts new file mode 100644 index 0000000000000..bb6c471591417 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts @@ -0,0 +1,273 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IUiSettingsClient } from 'kibana/server'; +import { i18n } from '@kbn/i18n'; +import { BaseAlert } from './base_alert'; +import { + AlertData, + AlertCluster, + AlertState, + AlertMessage, + AlertMessageLinkToken, + AlertInstanceState, + LegacyAlert, +} from './types'; +import { AlertInstance } from '../../../alerts/server'; +import { INDEX_ALERTS, ALERT_CLUSTER_HEALTH } from '../../common/constants'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { AlertMessageTokenType, AlertClusterHealthType } from '../../common/enums'; +import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { mapLegacySeverity } from '../lib/alerts/map_legacy_severity'; +import { CommonAlertParams } from '../../common/types'; + +const RED_STATUS_MESSAGE = i18n.translate('xpack.monitoring.alerts.clusterHealth.redMessage', { + defaultMessage: 'Allocate missing primary and replica shards', +}); + +const YELLOW_STATUS_MESSAGE = i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.yellowMessage', + { + defaultMessage: 'Allocate missing replica shards', + } +); + +const WATCH_NAME = 'elasticsearch_cluster_status'; + +export class ClusterHealthAlert extends BaseAlert { + public type = ALERT_CLUSTER_HEALTH; + public label = i18n.translate('xpack.monitoring.alerts.clusterHealth.label', { + defaultMessage: 'Cluster health', + }); + public isLegacy = true; + + protected actionVariables = [ + { + name: 'internalShortMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.actionVariables.internalShortMessage', + { + defaultMessage: 'The short internal message generated by Elastic.', + } + ), + }, + { + name: 'internalFullMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.actionVariables.internalFullMessage', + { + defaultMessage: 'The full internal message generated by Elastic.', + } + ), + }, + { + name: 'state', + description: i18n.translate('xpack.monitoring.alerts.clusterHealth.actionVariables.state', { + defaultMessage: 'The current state of the alert.', + }), + }, + { + name: 'clusterHealth', + description: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.actionVariables.clusterHealth', + { + defaultMessage: 'The health of the cluster.', + } + ), + }, + { + name: 'clusterName', + description: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.actionVariables.clusterName', + { + defaultMessage: 'The cluster to which the nodes belong.', + } + ), + }, + { + name: 'action', + description: i18n.translate('xpack.monitoring.alerts.clusterHealth.actionVariables.action', { + defaultMessage: 'The recommended action for this alert.', + }), + }, + { + name: 'actionPlain', + description: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.actionVariables.actionPlain', + { + defaultMessage: 'The recommended action for this alert, without any markdown.', + } + ), + }, + ]; + + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + uiSettings: IUiSettingsClient, + availableCcs: string[] + ): Promise { + let alertIndexPattern = INDEX_ALERTS; + if (availableCcs) { + alertIndexPattern = getCcsIndexPattern(alertIndexPattern, availableCcs); + } + const legacyAlerts = await fetchLegacyAlerts( + callCluster, + clusters, + alertIndexPattern, + WATCH_NAME, + this.config.ui.max_bucket_size + ); + return legacyAlerts.reduce((accum: AlertData[], legacyAlert) => { + accum.push({ + instanceKey: `${legacyAlert.metadata.cluster_uuid}`, + clusterUuid: legacyAlert.metadata.cluster_uuid, + shouldFire: !legacyAlert.resolved_timestamp, + severity: mapLegacySeverity(legacyAlert.metadata.severity), + meta: legacyAlert, + ccs: null, + }); + return accum; + }, []); + } + + private getHealth(legacyAlert: LegacyAlert) { + const prefixStr = 'Elasticsearch cluster status is '; + return legacyAlert.prefix.slice( + legacyAlert.prefix.indexOf(prefixStr) + prefixStr.length, + legacyAlert.prefix.length - 1 + ) as AlertClusterHealthType; + } + + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { + const legacyAlert = item.meta as LegacyAlert; + const health = this.getHealth(legacyAlert); + if (!alertState.ui.isFiring) { + return { + text: i18n.translate('xpack.monitoring.alerts.clusterHealth.ui.resolvedMessage', { + defaultMessage: `Elasticsearch cluster health is green.`, + }), + }; + } + + return { + text: i18n.translate('xpack.monitoring.alerts.clusterHealth.ui.firingMessage', { + defaultMessage: `Elasticsearch cluster health is {health}.`, + values: { + health, + }, + }), + nextSteps: [ + { + text: i18n.translate('xpack.monitoring.alerts.clusterHealth.ui.nextSteps.message1', { + defaultMessage: `{message}. #start_linkView now#end_link`, + values: { + message: + health === AlertClusterHealthType.Red ? RED_STATUS_MESSAGE : YELLOW_STATUS_MESSAGE, + }, + }), + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: AlertMessageTokenType.Link, + url: 'elasticsearch/indices', + } as AlertMessageLinkToken, + ], + }, + ], + }; + } + + protected async executeActions( + instance: AlertInstance, + instanceState: AlertInstanceState, + item: AlertData, + cluster: AlertCluster + ) { + if (instanceState.alertStates.length === 0) { + return; + } + const alertState = instanceState.alertStates[0]; + const legacyAlert = item.meta as LegacyAlert; + const health = this.getHealth(legacyAlert); + if (!alertState.ui.isFiring) { + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.resolved.internalShortMessage', + { + defaultMessage: `Cluster health alert is resolved for {clusterName}.`, + values: { + clusterName: cluster.clusterName, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.resolved.internalFullMessage', + { + defaultMessage: `Cluster health alert is resolved for {clusterName}.`, + values: { + clusterName: cluster.clusterName, + }, + } + ), + state: i18n.translate('xpack.monitoring.alerts.clusterHealth.resolved', { + defaultMessage: `resolved`, + }), + clusterHealth: health, + clusterName: cluster.clusterName, + }); + } else { + const actionText = + health === AlertClusterHealthType.Red + ? i18n.translate('xpack.monitoring.alerts.clusterHealth.action.danger', { + defaultMessage: `Allocate missing primary and replica shards.`, + }) + : i18n.translate('xpack.monitoring.alerts.clusterHealth.action.warning', { + defaultMessage: `Allocate missing replica shards.`, + }); + const globalState = [`cluster_uuid:${cluster.clusterUuid}`]; + if (alertState.ccs) { + globalState.push(`ccs:${alertState.ccs}`); + } + const url = `${this.kibanaUrl}/app/monitoring#elasticsearch/indices?_g=(${globalState.join( + ',' + )})`; + const action = `[${actionText}](${url})`; + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.firing.internalShortMessage', + { + defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {actionText}`, + values: { + clusterName: cluster.clusterName, + health, + actionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.firing.internalFullMessage', + { + defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {action}`, + values: { + clusterName: cluster.clusterName, + health, + action, + }, + } + ), + state: i18n.translate('xpack.monitoring.alerts.clusterHealth.firing', { + defaultMessage: `firing`, + }), + clusterHealth: health, + clusterName: cluster.clusterName, + action, + actionPlain: actionText, + }); + } + } +} diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_state.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_state.test.ts deleted file mode 100644 index 6262036037712..0000000000000 --- a/x-pack/plugins/monitoring/server/alerts/cluster_state.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { Logger } from 'src/core/server'; -import { getClusterState } from './cluster_state'; -import { ALERT_TYPE_CLUSTER_STATE } from '../../common/constants'; -import { AlertCommonParams, AlertCommonState, AlertClusterStatePerClusterState } from './types'; -import { getPreparedAlert } from '../lib/alerts/get_prepared_alert'; -import { executeActions } from '../lib/alerts/cluster_state.lib'; -import { AlertClusterStateState } from './enums'; -import { alertsMock, AlertServicesMock } from '../../../alerts/server/mocks'; - -jest.mock('../lib/alerts/cluster_state.lib', () => ({ - executeActions: jest.fn(), - getUiMessage: jest.fn(), -})); - -jest.mock('../lib/alerts/get_prepared_alert', () => ({ - getPreparedAlert: jest.fn(() => { - return { - emailAddress: 'foo@foo.com', - }; - }), -})); - -describe('getClusterState', () => { - const services: AlertServicesMock = alertsMock.createAlertServices(); - - const params: AlertCommonParams = { - dateFormat: 'YYYY', - timezone: 'UTC', - }; - - const emailAddress = 'foo@foo.com'; - const clusterUuid = 'kdksdfj434'; - const clusterName = 'monitoring_test'; - const cluster = { clusterUuid, clusterName }; - - async function setupAlert( - previousState: AlertClusterStateState, - newState: AlertClusterStateState - ): Promise { - const logger: Logger = { - warn: jest.fn(), - log: jest.fn(), - debug: jest.fn(), - trace: jest.fn(), - error: jest.fn(), - fatal: jest.fn(), - info: jest.fn(), - get: jest.fn(), - }; - const getLogger = (): Logger => logger; - const ccrEnabled = false; - (getPreparedAlert as jest.Mock).mockImplementation(() => ({ - emailAddress, - data: [ - { - state: newState, - clusterUuid, - }, - ], - clusters: [cluster], - })); - - const alert = getClusterState(null as any, null as any, getLogger, ccrEnabled); - const state: AlertCommonState = { - [clusterUuid]: { - state: previousState, - ui: { - isFiring: false, - severity: 0, - message: null, - resolvedMS: 0, - lastCheckedMS: 0, - triggeredMS: 0, - }, - } as AlertClusterStatePerClusterState, - }; - - return (await alert.executor({ services, params, state } as any)) as AlertCommonState; - } - - afterEach(() => { - (executeActions as jest.Mock).mockClear(); - }); - - it('should configure the alert properly', () => { - const alert = getClusterState(null as any, null as any, jest.fn(), false); - expect(alert.id).toBe(ALERT_TYPE_CLUSTER_STATE); - expect(alert.actionGroups).toEqual([{ id: 'default', name: 'Default' }]); - }); - - it('should alert if green -> yellow', async () => { - const result = await setupAlert(AlertClusterStateState.Green, AlertClusterStateState.Yellow); - expect(executeActions).toHaveBeenCalledWith( - services.alertInstanceFactory(ALERT_TYPE_CLUSTER_STATE), - cluster, - AlertClusterStateState.Yellow, - emailAddress - ); - const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState; - expect(clusterResult.state).toBe(AlertClusterStateState.Yellow); - expect(clusterResult.ui.isFiring).toBe(true); - expect(clusterResult.ui.resolvedMS).toBe(0); - }); - - it('should alert if yellow -> green', async () => { - const result = await setupAlert(AlertClusterStateState.Yellow, AlertClusterStateState.Green); - expect(executeActions).toHaveBeenCalledWith( - services.alertInstanceFactory(ALERT_TYPE_CLUSTER_STATE), - cluster, - AlertClusterStateState.Green, - emailAddress, - true - ); - const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState; - expect(clusterResult.state).toBe(AlertClusterStateState.Green); - expect(clusterResult.ui.resolvedMS).toBeGreaterThan(0); - }); - - it('should alert if green -> red', async () => { - const result = await setupAlert(AlertClusterStateState.Green, AlertClusterStateState.Red); - expect(executeActions).toHaveBeenCalledWith( - services.alertInstanceFactory(ALERT_TYPE_CLUSTER_STATE), - cluster, - AlertClusterStateState.Red, - emailAddress - ); - const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState; - expect(clusterResult.state).toBe(AlertClusterStateState.Red); - expect(clusterResult.ui.isFiring).toBe(true); - expect(clusterResult.ui.resolvedMS).toBe(0); - }); - - it('should alert if red -> green', async () => { - const result = await setupAlert(AlertClusterStateState.Red, AlertClusterStateState.Green); - expect(executeActions).toHaveBeenCalledWith( - services.alertInstanceFactory(ALERT_TYPE_CLUSTER_STATE), - cluster, - AlertClusterStateState.Green, - emailAddress, - true - ); - const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState; - expect(clusterResult.state).toBe(AlertClusterStateState.Green); - expect(clusterResult.ui.resolvedMS).toBeGreaterThan(0); - }); - - it('should not alert if red -> yellow', async () => { - const result = await setupAlert(AlertClusterStateState.Red, AlertClusterStateState.Yellow); - expect(executeActions).not.toHaveBeenCalled(); - const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState; - expect(clusterResult.state).toBe(AlertClusterStateState.Red); - expect(clusterResult.ui.resolvedMS).toBe(0); - }); - - it('should not alert if yellow -> red', async () => { - const result = await setupAlert(AlertClusterStateState.Yellow, AlertClusterStateState.Red); - expect(executeActions).not.toHaveBeenCalled(); - const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState; - expect(clusterResult.state).toBe(AlertClusterStateState.Yellow); - expect(clusterResult.ui.resolvedMS).toBe(0); - }); - - it('should not alert if green -> green', async () => { - const result = await setupAlert(AlertClusterStateState.Green, AlertClusterStateState.Green); - expect(executeActions).not.toHaveBeenCalled(); - const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState; - expect(clusterResult.state).toBe(AlertClusterStateState.Green); - expect(clusterResult.ui.resolvedMS).toBe(0); - }); -}); diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_state.ts b/x-pack/plugins/monitoring/server/alerts/cluster_state.ts deleted file mode 100644 index c357a5878b93a..0000000000000 --- a/x-pack/plugins/monitoring/server/alerts/cluster_state.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import moment from 'moment-timezone'; -import { i18n } from '@kbn/i18n'; -import { Logger, ILegacyCustomClusterClient, UiSettingsServiceStart } from 'src/core/server'; -import { ALERT_TYPE_CLUSTER_STATE } from '../../common/constants'; -import { AlertType } from '../../../alerts/server'; -import { executeActions, getUiMessage } from '../lib/alerts/cluster_state.lib'; -import { - AlertCommonExecutorOptions, - AlertCommonState, - AlertClusterStatePerClusterState, - AlertCommonCluster, -} from './types'; -import { AlertClusterStateState } from './enums'; -import { getPreparedAlert } from '../lib/alerts/get_prepared_alert'; -import { fetchClusterState } from '../lib/alerts/fetch_cluster_state'; - -export const getClusterState = ( - getUiSettingsService: () => Promise, - monitoringCluster: ILegacyCustomClusterClient, - getLogger: (...scopes: string[]) => Logger, - ccsEnabled: boolean -): AlertType => { - const logger = getLogger(ALERT_TYPE_CLUSTER_STATE); - return { - id: ALERT_TYPE_CLUSTER_STATE, - name: 'Monitoring Alert - Cluster Status', - actionGroups: [ - { - id: 'default', - name: i18n.translate('xpack.monitoring.alerts.clusterState.actionGroups.default', { - defaultMessage: 'Default', - }), - }, - ], - producer: 'monitoring', - defaultActionGroupId: 'default', - async executor({ - services, - params, - state, - }: AlertCommonExecutorOptions): Promise { - logger.debug( - `Firing alert with params: ${JSON.stringify(params)} and state: ${JSON.stringify(state)}` - ); - - const preparedAlert = await getPreparedAlert( - ALERT_TYPE_CLUSTER_STATE, - getUiSettingsService, - monitoringCluster, - logger, - ccsEnabled, - services, - fetchClusterState - ); - - if (!preparedAlert) { - return state; - } - - const { emailAddress, data: states, clusters } = preparedAlert; - - const result: AlertCommonState = { ...state }; - const defaultAlertState: AlertClusterStatePerClusterState = { - state: AlertClusterStateState.Green, - ui: { - isFiring: false, - message: null, - severity: 0, - resolvedMS: 0, - triggeredMS: 0, - lastCheckedMS: 0, - }, - }; - - for (const clusterState of states) { - const alertState: AlertClusterStatePerClusterState = - (state[clusterState.clusterUuid] as AlertClusterStatePerClusterState) || - defaultAlertState; - const cluster = clusters.find( - (c: AlertCommonCluster) => c.clusterUuid === clusterState.clusterUuid - ); - if (!cluster) { - logger.warn(`Unable to find cluster for clusterUuid='${clusterState.clusterUuid}'`); - continue; - } - const isNonGreen = clusterState.state !== AlertClusterStateState.Green; - const severity = clusterState.state === AlertClusterStateState.Red ? 2100 : 1100; - - const ui = alertState.ui; - let triggered = ui.triggeredMS; - let resolved = ui.resolvedMS; - let message = ui.message || {}; - let lastState = alertState.state; - const instance = services.alertInstanceFactory(ALERT_TYPE_CLUSTER_STATE); - - if (isNonGreen) { - if (lastState === AlertClusterStateState.Green) { - logger.debug(`Cluster state changed from green to ${clusterState.state}`); - executeActions(instance, cluster, clusterState.state, emailAddress); - lastState = clusterState.state; - triggered = moment().valueOf(); - } - message = getUiMessage(clusterState.state); - resolved = 0; - } else if (!isNonGreen && lastState !== AlertClusterStateState.Green) { - logger.debug(`Cluster state changed from ${lastState} to green`); - executeActions(instance, cluster, clusterState.state, emailAddress, true); - lastState = clusterState.state; - message = getUiMessage(clusterState.state, true); - resolved = moment().valueOf(); - } - - result[clusterState.clusterUuid] = { - state: lastState, - ui: { - message, - isFiring: isNonGreen, - severity, - resolvedMS: resolved, - triggeredMS: triggered, - lastCheckedMS: moment().valueOf(), - }, - } as AlertClusterStatePerClusterState; - } - - return result; - }, - }; -}; diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts new file mode 100644 index 0000000000000..f0d11abab1492 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import { CpuUsageAlert } from './cpu_usage_alert'; +import { ALERT_CPU_USAGE } from '../../common/constants'; +import { fetchCpuUsageNodeStats } from '../lib/alerts/fetch_cpu_usage_node_stats'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; + +const RealDate = Date; + +jest.mock('../lib/alerts/fetch_cpu_usage_node_stats', () => ({ + fetchCpuUsageNodeStats: jest.fn(), +})); +jest.mock('../lib/alerts/fetch_clusters', () => ({ + fetchClusters: jest.fn(), +})); + +describe('CpuUsageAlert', () => { + it('should have defaults', () => { + const alert = new CpuUsageAlert(); + expect(alert.type).toBe(ALERT_CPU_USAGE); + expect(alert.label).toBe('CPU Usage'); + expect(alert.defaultThrottle).toBe('1m'); + // @ts-ignore + expect(alert.defaultParams).toStrictEqual({ threshold: 90, duration: '5m' }); + // @ts-ignore + expect(alert.actionVariables).toStrictEqual([ + { + name: 'internalShortMessage', + description: 'The short internal message generated by Elastic.', + }, + { + name: 'internalFullMessage', + description: 'The full internal message generated by Elastic.', + }, + { name: 'state', description: 'The current state of the alert.' }, + { name: 'nodes', description: 'The list of nodes reporting high cpu usage.' }, + { name: 'count', description: 'The number of nodes reporting high cpu usage.' }, + { name: 'clusterName', description: 'The cluster to which the nodes belong.' }, + { name: 'action', description: 'The recommended action for this alert.' }, + { + name: 'actionPlain', + description: 'The recommended action for this alert, without any markdown.', + }, + ]); + }); + + describe('execute', () => { + function FakeDate() {} + FakeDate.prototype.valueOf = () => 1; + + const clusterUuid = 'abc123'; + const clusterName = 'testCluster'; + const nodeId = 'myNodeId'; + const nodeName = 'myNodeName'; + const cpuUsage = 91; + const stat = { + clusterUuid, + nodeId, + nodeName, + cpuUsage, + }; + const getUiSettingsService = () => ({ + asScopedToClient: jest.fn(), + }); + const getLogger = () => ({ + debug: jest.fn(), + }); + const monitoringCluster = null; + const config = { + ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + }; + const kibanaUrl = 'http://localhost:5601'; + + const replaceState = jest.fn(); + const scheduleActions = jest.fn(); + const getState = jest.fn(); + const executorOptions = { + services: { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, + state: {}, + }; + + beforeEach(() => { + // @ts-ignore + Date = FakeDate; + (fetchCpuUsageNodeStats as jest.Mock).mockImplementation(() => { + return [stat]; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + replaceState.mockReset(); + scheduleActions.mockReset(); + getState.mockReset(); + }); + + it('should fire actions', async () => { + const alert = new CpuUsageAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + const count = 1; + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid, clusterName }, + cpuUsage, + nodeId, + nodeName, + ui: { + isFiring: true, + message: { + text: + 'Node #start_linkmyNodeName#end_link is reporting cpu usage of 91.00% at #absolute', + nextSteps: [ + { + text: '#start_linkCheck hot threads#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'docLink', + partialUrl: + '{elasticWebsiteUrl}/guide/en/elasticsearch/reference/{docLinkVersion}/cluster-nodes-hot-threads.html', + }, + ], + }, + { + text: '#start_linkCheck long running tasks#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'docLink', + partialUrl: + '{elasticWebsiteUrl}/guide/en/elasticsearch/reference/{docLinkVersion}/tasks.html', + }, + ], + }, + ], + tokens: [ + { + startToken: '#absolute', + type: 'time', + isAbsolute: true, + isRelative: false, + timestamp: 1, + }, + { + startToken: '#start_link', + endToken: '#end_link', + type: 'link', + url: 'elasticsearch/nodes/myNodeId', + }, + ], + }, + severity: 'danger', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: `CPU usage alert is firing for ${count} node(s) in cluster: ${clusterName}. [View nodes](http://localhost:5601/app/monitoring#elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid}))`, + internalShortMessage: `CPU usage alert is firing for ${count} node(s) in cluster: ${clusterName}. Verify CPU levels across affected nodes.`, + action: `[View nodes](http://localhost:5601/app/monitoring#elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid}))`, + actionPlain: 'Verify CPU levels across affected nodes.', + clusterName, + count, + nodes: `${nodeName}:${cpuUsage.toFixed(2)}`, + state: 'firing', + }); + }); + + it('should not fire actions if under threshold', async () => { + (fetchCpuUsageNodeStats as jest.Mock).mockImplementation(() => { + return [ + { + ...stat, + cpuUsage: 1, + }, + ]; + }); + const alert = new CpuUsageAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + ccs: undefined, + cluster: { + clusterUuid, + clusterName, + }, + cpuUsage: 1, + nodeId, + nodeName, + ui: { + isFiring: false, + lastCheckedMS: 0, + message: null, + resolvedMS: 0, + severity: 'danger', + triggeredMS: 0, + }, + }, + ], + }); + expect(scheduleActions).not.toHaveBeenCalled(); + }); + + it('should resolve with a resolved message', async () => { + (fetchCpuUsageNodeStats as jest.Mock).mockImplementation(() => { + return [ + { + ...stat, + cpuUsage: 1, + }, + ]; + }); + (getState as jest.Mock).mockImplementation(() => { + return { + alertStates: [ + { + cluster: { + clusterUuid, + clusterName, + }, + ccs: null, + cpuUsage: 91, + nodeId, + nodeName, + ui: { + isFiring: true, + message: null, + severity: 'danger', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }; + }); + const alert = new CpuUsageAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + const count = 1; + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid, clusterName }, + ccs: null, + cpuUsage: 1, + nodeId, + nodeName, + ui: { + isFiring: false, + message: { + text: + 'The cpu usage on node myNodeName is now under the threshold, currently reporting at 1.00% as of #resolved', + tokens: [ + { + startToken: '#resolved', + type: 'time', + isAbsolute: true, + isRelative: false, + timestamp: 1, + }, + ], + }, + severity: 'danger', + resolvedMS: 1, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: `CPU usage alert is resolved for ${count} node(s) in cluster: ${clusterName}.`, + internalShortMessage: `CPU usage alert is resolved for ${count} node(s) in cluster: ${clusterName}.`, + clusterName, + count, + nodes: `${nodeName}:1.00`, + state: 'resolved', + }); + }); + + it('should handle ccs', async () => { + const ccs = 'testCluster'; + (fetchCpuUsageNodeStats as jest.Mock).mockImplementation(() => { + return [ + { + ...stat, + ccs, + }, + ]; + }); + const alert = new CpuUsageAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + const count = 1; + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: `CPU usage alert is firing for ${count} node(s) in cluster: ${clusterName}. [View nodes](http://localhost:5601/app/monitoring#elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid},ccs:${ccs}))`, + internalShortMessage: `CPU usage alert is firing for ${count} node(s) in cluster: ${clusterName}. Verify CPU levels across affected nodes.`, + action: `[View nodes](http://localhost:5601/app/monitoring#elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid},ccs:${ccs}))`, + actionPlain: 'Verify CPU levels across affected nodes.', + clusterName, + count, + nodes: `${nodeName}:${cpuUsage.toFixed(2)}`, + state: 'firing', + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts new file mode 100644 index 0000000000000..9171745fba747 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts @@ -0,0 +1,451 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IUiSettingsClient, Logger } from 'kibana/server'; +import { i18n } from '@kbn/i18n'; +import { BaseAlert } from './base_alert'; +import { + AlertData, + AlertCluster, + AlertState, + AlertMessage, + AlertCpuUsageState, + AlertCpuUsageNodeStats, + AlertMessageTimeToken, + AlertMessageLinkToken, + AlertInstanceState, + AlertMessageDocLinkToken, +} from './types'; +import { AlertInstance, AlertServices } from '../../../alerts/server'; +import { INDEX_PATTERN_ELASTICSEARCH, ALERT_CPU_USAGE } from '../../common/constants'; +import { fetchCpuUsageNodeStats } from '../lib/alerts/fetch_cpu_usage_node_stats'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { AlertMessageTokenType, AlertSeverity, AlertParamType } from '../../common/enums'; +import { RawAlertInstance } from '../../../alerts/common'; +import { parseDuration } from '../../../alerts/common/parse_duration'; +import { + CommonAlertFilter, + CommonAlertCpuUsageFilter, + CommonAlertParams, + CommonAlertParamDetail, +} from '../../common/types'; + +const RESOLVED = i18n.translate('xpack.monitoring.alerts.cpuUsage.resolved', { + defaultMessage: 'resolved', +}); +const FIRING = i18n.translate('xpack.monitoring.alerts.cpuUsage.firing', { + defaultMessage: 'firing', +}); + +const DEFAULT_THRESHOLD = 90; +const DEFAULT_DURATION = '5m'; + +interface CpuUsageParams { + threshold: number; + duration: string; +} + +export class CpuUsageAlert extends BaseAlert { + public static paramDetails = { + threshold: { + label: i18n.translate('xpack.monitoring.alerts.cpuUsage.paramDetails.threshold.label', { + defaultMessage: `Notify when CPU is over`, + }), + type: AlertParamType.Percentage, + } as CommonAlertParamDetail, + duration: { + label: i18n.translate('xpack.monitoring.alerts.cpuUsage.paramDetails.duration.label', { + defaultMessage: `Look at the average over`, + }), + type: AlertParamType.Duration, + } as CommonAlertParamDetail, + }; + + public type = ALERT_CPU_USAGE; + public label = i18n.translate('xpack.monitoring.alerts.cpuUsage.label', { + defaultMessage: 'CPU Usage', + }); + + protected defaultParams: CpuUsageParams = { + threshold: DEFAULT_THRESHOLD, + duration: DEFAULT_DURATION, + }; + + protected actionVariables = [ + { + name: 'internalShortMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.cpuUsage.actionVariables.internalShortMessage', + { + defaultMessage: 'The short internal message generated by Elastic.', + } + ), + }, + { + name: 'internalFullMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.cpuUsage.actionVariables.internalFullMessage', + { + defaultMessage: 'The full internal message generated by Elastic.', + } + ), + }, + { + name: 'state', + description: i18n.translate('xpack.monitoring.alerts.cpuUsage.actionVariables.state', { + defaultMessage: 'The current state of the alert.', + }), + }, + { + name: 'nodes', + description: i18n.translate('xpack.monitoring.alerts.cpuUsage.actionVariables.nodes', { + defaultMessage: 'The list of nodes reporting high cpu usage.', + }), + }, + { + name: 'count', + description: i18n.translate('xpack.monitoring.alerts.cpuUsage.actionVariables.count', { + defaultMessage: 'The number of nodes reporting high cpu usage.', + }), + }, + { + name: 'clusterName', + description: i18n.translate('xpack.monitoring.alerts.cpuUsage.actionVariables.clusterName', { + defaultMessage: 'The cluster to which the nodes belong.', + }), + }, + { + name: 'action', + description: i18n.translate('xpack.monitoring.alerts.cpuUsage.actionVariables.action', { + defaultMessage: 'The recommended action for this alert.', + }), + }, + { + name: 'actionPlain', + description: i18n.translate('xpack.monitoring.alerts.cpuUsage.actionVariables.actionPlain', { + defaultMessage: 'The recommended action for this alert, without any markdown.', + }), + }, + ]; + + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + uiSettings: IUiSettingsClient, + availableCcs: string[] + ): Promise { + let esIndexPattern = INDEX_PATTERN_ELASTICSEARCH; + if (availableCcs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + const duration = parseDuration(((params as unknown) as CpuUsageParams).duration); + const endMs = +new Date(); + const startMs = endMs - duration; + const stats = await fetchCpuUsageNodeStats( + callCluster, + clusters, + esIndexPattern, + startMs, + endMs, + this.config.ui.max_bucket_size + ); + return stats.map((stat) => { + let cpuUsage = 0; + if (this.config.ui.container.elasticsearch.enabled) { + cpuUsage = + (stat.containerUsage / (stat.containerPeriods * stat.containerQuota * 1000)) * 100; + } else { + cpuUsage = stat.cpuUsage; + } + + return { + instanceKey: `${stat.clusterUuid}:${stat.nodeId}`, + clusterUuid: stat.clusterUuid, + shouldFire: cpuUsage > params.threshold, + severity: AlertSeverity.Danger, + meta: stat, + ccs: stat.ccs, + }; + }); + } + + protected filterAlertInstance(alertInstance: RawAlertInstance, filters: CommonAlertFilter[]) { + const alertInstanceState = (alertInstance.state as unknown) as AlertInstanceState; + if (filters && filters.length) { + for (const _filter of filters) { + const filter = _filter as CommonAlertCpuUsageFilter; + if (filter && filter.nodeUuid) { + let nodeExistsInStates = false; + for (const state of alertInstanceState.alertStates) { + if ((state as AlertCpuUsageState).nodeId === filter.nodeUuid) { + nodeExistsInStates = true; + break; + } + } + if (!nodeExistsInStates) { + return false; + } + } + } + } + return true; + } + + protected getDefaultAlertState(cluster: AlertCluster, item: AlertData): AlertState { + const base = super.getDefaultAlertState(cluster, item); + return { + ...base, + ui: { + ...base.ui, + severity: AlertSeverity.Danger, + }, + }; + } + + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { + const stat = item.meta as AlertCpuUsageNodeStats; + if (!alertState.ui.isFiring) { + return { + text: i18n.translate('xpack.monitoring.alerts.cpuUsage.ui.resolvedMessage', { + defaultMessage: `The cpu usage on node {nodeName} is now under the threshold, currently reporting at {cpuUsage}% as of #resolved`, + values: { + nodeName: stat.nodeName, + cpuUsage: stat.cpuUsage.toFixed(2), + }, + }), + tokens: [ + { + startToken: '#resolved', + type: AlertMessageTokenType.Time, + isAbsolute: true, + isRelative: false, + timestamp: alertState.ui.resolvedMS, + } as AlertMessageTimeToken, + ], + }; + } + return { + text: i18n.translate('xpack.monitoring.alerts.cpuUsage.ui.firingMessage', { + defaultMessage: `Node #start_link{nodeName}#end_link is reporting cpu usage of {cpuUsage}% at #absolute`, + values: { + nodeName: stat.nodeName, + cpuUsage: stat.cpuUsage.toFixed(2), + }, + }), + nextSteps: [ + { + text: i18n.translate('xpack.monitoring.alerts.cpuUsage.ui.nextSteps.hotThreads', { + defaultMessage: `#start_linkCheck hot threads#end_link`, + }), + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: AlertMessageTokenType.DocLink, + partialUrl: `{elasticWebsiteUrl}/guide/en/elasticsearch/reference/{docLinkVersion}/cluster-nodes-hot-threads.html`, + } as AlertMessageDocLinkToken, + ], + }, + { + text: i18n.translate('xpack.monitoring.alerts.cpuUsage.ui.nextSteps.runningTasks', { + defaultMessage: `#start_linkCheck long running tasks#end_link`, + }), + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: AlertMessageTokenType.DocLink, + partialUrl: `{elasticWebsiteUrl}/guide/en/elasticsearch/reference/{docLinkVersion}/tasks.html`, + } as AlertMessageDocLinkToken, + ], + }, + ], + tokens: [ + { + startToken: '#absolute', + type: AlertMessageTokenType.Time, + isAbsolute: true, + isRelative: false, + timestamp: alertState.ui.triggeredMS, + } as AlertMessageTimeToken, + { + startToken: '#start_link', + endToken: '#end_link', + type: AlertMessageTokenType.Link, + url: `elasticsearch/nodes/${stat.nodeId}`, + } as AlertMessageLinkToken, + ], + }; + } + + protected executeActions( + instance: AlertInstance, + instanceState: AlertInstanceState, + item: AlertData | null, + cluster: AlertCluster + ) { + if (instanceState.alertStates.length === 0) { + return; + } + + const nodes = instanceState.alertStates + .map((_state) => { + const state = _state as AlertCpuUsageState; + return `${state.nodeName}:${state.cpuUsage.toFixed(2)}`; + }) + .join(','); + + const ccs = instanceState.alertStates.reduce((accum: string, state): string => { + if (state.ccs) { + return state.ccs; + } + return accum; + }, ''); + + const count = instanceState.alertStates.length; + if (!instanceState.alertStates[0].ui.isFiring) { + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.cpuUsage.resolved.internalShortMessage', + { + defaultMessage: `CPU usage alert is resolved for {count} node(s) in cluster: {clusterName}.`, + values: { + count, + clusterName: cluster.clusterName, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.cpuUsage.resolved.internalFullMessage', + { + defaultMessage: `CPU usage alert is resolved for {count} node(s) in cluster: {clusterName}.`, + values: { + count, + clusterName: cluster.clusterName, + }, + } + ), + state: RESOLVED, + nodes, + count, + clusterName: cluster.clusterName, + }); + } else { + const shortActionText = i18n.translate('xpack.monitoring.alerts.cpuUsage.shortAction', { + defaultMessage: 'Verify CPU levels across affected nodes.', + }); + const fullActionText = i18n.translate('xpack.monitoring.alerts.cpuUsage.fullAction', { + defaultMessage: 'View nodes', + }); + const globalState = [`cluster_uuid:${cluster.clusterUuid}`]; + if (ccs) { + globalState.push(`ccs:${ccs}`); + } + const url = `${this.kibanaUrl}/app/monitoring#elasticsearch/nodes?_g=(${globalState.join( + ',' + )})`; + const action = `[${fullActionText}](${url})`; + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.cpuUsage.firing.internalShortMessage', + { + defaultMessage: `CPU usage alert is firing for {count} node(s) in cluster: {clusterName}. {shortActionText}`, + values: { + count, + clusterName: cluster.clusterName, + shortActionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.cpuUsage.firing.internalFullMessage', + { + defaultMessage: `CPU usage alert is firing for {count} node(s) in cluster: {clusterName}. {action}`, + values: { + count, + clusterName: cluster.clusterName, + action, + }, + } + ), + state: FIRING, + nodes, + count, + clusterName: cluster.clusterName, + action, + actionPlain: shortActionText, + }); + } + } + + protected processData( + data: AlertData[], + clusters: AlertCluster[], + services: AlertServices, + logger: Logger + ) { + for (const cluster of clusters) { + const nodes = data.filter((_item) => _item.clusterUuid === cluster.clusterUuid); + if (nodes.length === 0) { + continue; + } + + const instance = services.alertInstanceFactory(`${this.type}:${cluster.clusterUuid}`); + const state = (instance.getState() as unknown) as AlertInstanceState; + const alertInstanceState: AlertInstanceState = { alertStates: state?.alertStates || [] }; + let shouldExecuteActions = false; + for (const node of nodes) { + const stat = node.meta as AlertCpuUsageNodeStats; + let nodeState: AlertCpuUsageState; + const indexInState = alertInstanceState.alertStates.findIndex((alertState) => { + const nodeAlertState = alertState as AlertCpuUsageState; + return ( + nodeAlertState.cluster.clusterUuid === cluster.clusterUuid && + nodeAlertState.nodeId === (node.meta as AlertCpuUsageNodeStats).nodeId + ); + }); + if (indexInState > -1) { + nodeState = alertInstanceState.alertStates[indexInState] as AlertCpuUsageState; + } else { + nodeState = this.getDefaultAlertState(cluster, node) as AlertCpuUsageState; + } + + nodeState.cpuUsage = stat.cpuUsage; + nodeState.nodeId = stat.nodeId; + nodeState.nodeName = stat.nodeName; + + if (node.shouldFire) { + nodeState.ui.triggeredMS = new Date().valueOf(); + nodeState.ui.isFiring = true; + nodeState.ui.message = this.getUiMessage(nodeState, node); + nodeState.ui.severity = node.severity; + nodeState.ui.resolvedMS = 0; + shouldExecuteActions = true; + } else if (!node.shouldFire && nodeState.ui.isFiring) { + nodeState.ui.isFiring = false; + nodeState.ui.resolvedMS = new Date().valueOf(); + nodeState.ui.message = this.getUiMessage(nodeState, node); + shouldExecuteActions = true; + } + + if (indexInState === -1) { + alertInstanceState.alertStates.push(nodeState); + } else { + alertInstanceState.alertStates = [ + ...alertInstanceState.alertStates.slice(0, indexInState), + nodeState, + ...alertInstanceState.alertStates.slice(indexInState + 1), + ]; + } + } + + instance.replaceState(alertInstanceState); + if (shouldExecuteActions) { + this.executeActions(instance, alertInstanceState, null, cluster); + } + } + } +} diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts new file mode 100644 index 0000000000000..44684939ca261 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts @@ -0,0 +1,251 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ElasticsearchVersionMismatchAlert } from './elasticsearch_version_mismatch_alert'; +import { ALERT_ELASTICSEARCH_VERSION_MISMATCH } from '../../common/constants'; +import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; + +const RealDate = Date; + +jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ + fetchLegacyAlerts: jest.fn(), +})); +jest.mock('../lib/alerts/fetch_clusters', () => ({ + fetchClusters: jest.fn(), +})); + +describe('ElasticsearchVersionMismatchAlert', () => { + it('should have defaults', () => { + const alert = new ElasticsearchVersionMismatchAlert(); + expect(alert.type).toBe(ALERT_ELASTICSEARCH_VERSION_MISMATCH); + expect(alert.label).toBe('Elasticsearch version mismatch'); + expect(alert.defaultThrottle).toBe('1m'); + // @ts-ignore + expect(alert.actionVariables).toStrictEqual([ + { + name: 'internalShortMessage', + description: 'The short internal message generated by Elastic.', + }, + { + name: 'internalFullMessage', + description: 'The full internal message generated by Elastic.', + }, + { name: 'state', description: 'The current state of the alert.' }, + { + name: 'versionList', + description: 'The versions of Elasticsearch running in this cluster.', + }, + { name: 'clusterName', description: 'The cluster to which the nodes belong.' }, + { name: 'action', description: 'The recommended action for this alert.' }, + { + name: 'actionPlain', + description: 'The recommended action for this alert, without any markdown.', + }, + ]); + }); + + describe('execute', () => { + function FakeDate() {} + FakeDate.prototype.valueOf = () => 1; + + const clusterUuid = 'abc123'; + const clusterName = 'testCluster'; + const legacyAlert = { + prefix: 'This cluster is running with multiple versions of Elasticsearch.', + message: 'Versions: [8.0.0, 7.2.1].', + metadata: { + severity: 1000, + cluster_uuid: clusterUuid, + }, + }; + const getUiSettingsService = () => ({ + asScopedToClient: jest.fn(), + }); + const getLogger = () => ({ + debug: jest.fn(), + }); + const monitoringCluster = null; + const config = { + ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + }; + const kibanaUrl = 'http://localhost:5601'; + + const replaceState = jest.fn(); + const scheduleActions = jest.fn(); + const getState = jest.fn(); + const executorOptions = { + services: { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, + state: {}, + }; + + beforeEach(() => { + // @ts-ignore + Date = FakeDate; + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return [legacyAlert]; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + replaceState.mockReset(); + scheduleActions.mockReset(); + getState.mockReset(); + }); + + it('should fire actions', async () => { + const alert = new ElasticsearchVersionMismatchAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, + ccs: null, + ui: { + isFiring: true, + message: { + text: + 'Multiple versions of Elasticsearch ([8.0.0, 7.2.1]) running in this cluster.', + }, + severity: 'warning', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + action: + '[View nodes](http://localhost:5601/app/monitoring#elasticsearch/nodes?_g=(cluster_uuid:abc123))', + actionPlain: 'Verify you have the same version across all nodes.', + internalFullMessage: + 'Elasticsearch version mismatch alert is firing for testCluster. Elasticsearch is running [8.0.0, 7.2.1]. [View nodes](http://localhost:5601/app/monitoring#elasticsearch/nodes?_g=(cluster_uuid:abc123))', + internalShortMessage: + 'Elasticsearch version mismatch alert is firing for testCluster. Verify you have the same version across all nodes.', + versionList: '[8.0.0, 7.2.1]', + clusterName, + state: 'firing', + }); + }); + + it('should not fire actions if there is no legacy alert', async () => { + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return []; + }); + const alert = new ElasticsearchVersionMismatchAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).not.toHaveBeenCalledWith({}); + expect(scheduleActions).not.toHaveBeenCalled(); + }); + + it('should resolve with a resolved message', async () => { + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return [ + { + ...legacyAlert, + resolved_timestamp: 1, + }, + ]; + }); + (getState as jest.Mock).mockImplementation(() => { + return { + alertStates: [ + { + cluster: { + clusterUuid, + clusterName, + }, + ccs: null, + ui: { + isFiring: true, + message: null, + severity: 'danger', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }; + }); + const alert = new ElasticsearchVersionMismatchAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid, clusterName }, + ccs: null, + ui: { + isFiring: false, + message: { + text: 'All versions of Elasticsearch are the same in this cluster.', + }, + severity: 'danger', + resolvedMS: 1, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: 'Elasticsearch version mismatch alert is resolved for testCluster.', + internalShortMessage: 'Elasticsearch version mismatch alert is resolved for testCluster.', + clusterName, + state: 'resolved', + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts new file mode 100644 index 0000000000000..e3b952fbbe5d3 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts @@ -0,0 +1,263 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IUiSettingsClient } from 'kibana/server'; +import { i18n } from '@kbn/i18n'; +import { BaseAlert } from './base_alert'; +import { + AlertData, + AlertCluster, + AlertState, + AlertMessage, + AlertInstanceState, + LegacyAlert, +} from './types'; +import { AlertInstance } from '../../../alerts/server'; +import { INDEX_ALERTS, ALERT_ELASTICSEARCH_VERSION_MISMATCH } from '../../common/constants'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { AlertSeverity } from '../../common/enums'; +import { CommonAlertParams } from '../../common/types'; +import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; + +const WATCH_NAME = 'elasticsearch_version_mismatch'; +const RESOLVED = i18n.translate('xpack.monitoring.alerts.elasticsearchVersionMismatch.resolved', { + defaultMessage: 'resolved', +}); +const FIRING = i18n.translate('xpack.monitoring.alerts.elasticsearchVersionMismatch.firing', { + defaultMessage: 'firing', +}); + +export class ElasticsearchVersionMismatchAlert extends BaseAlert { + public type = ALERT_ELASTICSEARCH_VERSION_MISMATCH; + public label = i18n.translate('xpack.monitoring.alerts.elasticsearchVersionMismatch.label', { + defaultMessage: 'Elasticsearch version mismatch', + }); + public isLegacy = true; + + protected actionVariables = [ + { + name: 'internalShortMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.actionVariables.internalShortMessage', + { + defaultMessage: 'The short internal message generated by Elastic.', + } + ), + }, + { + name: 'internalFullMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.actionVariables.internalFullMessage', + { + defaultMessage: 'The full internal message generated by Elastic.', + } + ), + }, + { + name: 'state', + description: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.actionVariables.state', + { + defaultMessage: 'The current state of the alert.', + } + ), + }, + { + name: 'versionList', + description: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.actionVariables.clusterHealth', + { + defaultMessage: 'The versions of Elasticsearch running in this cluster.', + } + ), + }, + { + name: 'clusterName', + description: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.actionVariables.clusterName', + { + defaultMessage: 'The cluster to which the nodes belong.', + } + ), + }, + { + name: 'action', + description: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.actionVariables.action', + { + defaultMessage: 'The recommended action for this alert.', + } + ), + }, + { + name: 'actionPlain', + description: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.actionVariables.actionPlain', + { + defaultMessage: 'The recommended action for this alert, without any markdown.', + } + ), + }, + ]; + + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + uiSettings: IUiSettingsClient, + availableCcs: string[] + ): Promise { + let alertIndexPattern = INDEX_ALERTS; + if (availableCcs) { + alertIndexPattern = getCcsIndexPattern(alertIndexPattern, availableCcs); + } + + const legacyAlerts = await fetchLegacyAlerts( + callCluster, + clusters, + alertIndexPattern, + WATCH_NAME, + this.config.ui.max_bucket_size + ); + + return legacyAlerts.reduce((accum: AlertData[], legacyAlert) => { + const severity = AlertSeverity.Warning; + + accum.push({ + instanceKey: `${legacyAlert.metadata.cluster_uuid}`, + clusterUuid: legacyAlert.metadata.cluster_uuid, + shouldFire: !legacyAlert.resolved_timestamp, + severity, + meta: legacyAlert, + ccs: null, + }); + return accum; + }, []); + } + + private getVersions(legacyAlert: LegacyAlert) { + const prefixStr = 'Versions: '; + return legacyAlert.message.slice( + legacyAlert.message.indexOf(prefixStr) + prefixStr.length, + legacyAlert.message.length - 1 + ); + } + + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { + const legacyAlert = item.meta as LegacyAlert; + const versions = this.getVersions(legacyAlert); + if (!alertState.ui.isFiring) { + return { + text: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.ui.resolvedMessage', + { + defaultMessage: `All versions of Elasticsearch are the same in this cluster.`, + } + ), + }; + } + + const text = i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.ui.firingMessage', + { + defaultMessage: `Multiple versions of Elasticsearch ({versions}) running in this cluster.`, + values: { + versions, + }, + } + ); + + return { + text, + }; + } + + protected async executeActions( + instance: AlertInstance, + instanceState: AlertInstanceState, + item: AlertData, + cluster: AlertCluster + ) { + if (instanceState.alertStates.length === 0) { + return; + } + const alertState = instanceState.alertStates[0]; + const legacyAlert = item.meta as LegacyAlert; + const versions = this.getVersions(legacyAlert); + if (!alertState.ui.isFiring) { + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.resolved.internalShortMessage', + { + defaultMessage: `Elasticsearch version mismatch alert is resolved for {clusterName}.`, + values: { + clusterName: cluster.clusterName, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.resolved.internalFullMessage', + { + defaultMessage: `Elasticsearch version mismatch alert is resolved for {clusterName}.`, + values: { + clusterName: cluster.clusterName, + }, + } + ), + state: RESOLVED, + clusterName: cluster.clusterName, + }); + } else { + const shortActionText = i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.shortAction', + { + defaultMessage: 'Verify you have the same version across all nodes.', + } + ); + const fullActionText = i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.fullAction', + { + defaultMessage: 'View nodes', + } + ); + const globalState = [`cluster_uuid:${cluster.clusterUuid}`]; + if (alertState.ccs) { + globalState.push(`ccs:${alertState.ccs}`); + } + const url = `${this.kibanaUrl}/app/monitoring#elasticsearch/nodes?_g=(${globalState.join( + ',' + )})`; + const action = `[${fullActionText}](${url})`; + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage', + { + defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalFullMessage', + { + defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. Elasticsearch is running {versions}. {action}`, + values: { + clusterName: cluster.clusterName, + versions, + action, + }, + } + ), + state: FIRING, + clusterName: cluster.clusterName, + versionList: versions, + action, + actionPlain: shortActionText, + }); + } + } +} diff --git a/x-pack/plugins/monitoring/server/alerts/index.ts b/x-pack/plugins/monitoring/server/alerts/index.ts new file mode 100644 index 0000000000000..048e703d2222c --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { BaseAlert } from './base_alert'; +export { CpuUsageAlert } from './cpu_usage_alert'; +export { ClusterHealthAlert } from './cluster_health_alert'; +export { LicenseExpirationAlert } from './license_expiration_alert'; +export { NodesChangedAlert } from './nodes_changed_alert'; +export { ElasticsearchVersionMismatchAlert } from './elasticsearch_version_mismatch_alert'; +export { KibanaVersionMismatchAlert } from './kibana_version_mismatch_alert'; +export { LogstashVersionMismatchAlert } from './logstash_version_mismatch_alert'; +export { AlertsFactory, BY_TYPE } from './alerts_factory'; diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts new file mode 100644 index 0000000000000..6c56c7aa08d71 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts @@ -0,0 +1,253 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaVersionMismatchAlert } from './kibana_version_mismatch_alert'; +import { ALERT_KIBANA_VERSION_MISMATCH } from '../../common/constants'; +import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; + +const RealDate = Date; + +jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ + fetchLegacyAlerts: jest.fn(), +})); +jest.mock('../lib/alerts/fetch_clusters', () => ({ + fetchClusters: jest.fn(), +})); + +describe('KibanaVersionMismatchAlert', () => { + it('should have defaults', () => { + const alert = new KibanaVersionMismatchAlert(); + expect(alert.type).toBe(ALERT_KIBANA_VERSION_MISMATCH); + expect(alert.label).toBe('Kibana version mismatch'); + expect(alert.defaultThrottle).toBe('1m'); + // @ts-ignore + expect(alert.actionVariables).toStrictEqual([ + { + name: 'internalShortMessage', + description: 'The short internal message generated by Elastic.', + }, + { + name: 'internalFullMessage', + description: 'The full internal message generated by Elastic.', + }, + { name: 'state', description: 'The current state of the alert.' }, + { + name: 'versionList', + description: 'The versions of Kibana running in this cluster.', + }, + { + name: 'clusterName', + description: 'The cluster to which the instances belong.', + }, + { name: 'action', description: 'The recommended action for this alert.' }, + { + name: 'actionPlain', + description: 'The recommended action for this alert, without any markdown.', + }, + ]); + }); + + describe('execute', () => { + function FakeDate() {} + FakeDate.prototype.valueOf = () => 1; + + const clusterUuid = 'abc123'; + const clusterName = 'testCluster'; + const legacyAlert = { + prefix: 'This cluster is running with multiple versions of Kibana.', + message: 'Versions: [8.0.0, 7.2.1].', + metadata: { + severity: 1000, + cluster_uuid: clusterUuid, + }, + }; + const getUiSettingsService = () => ({ + asScopedToClient: jest.fn(), + }); + const getLogger = () => ({ + debug: jest.fn(), + }); + const monitoringCluster = null; + const config = { + ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + }; + const kibanaUrl = 'http://localhost:5601'; + + const replaceState = jest.fn(); + const scheduleActions = jest.fn(); + const getState = jest.fn(); + const executorOptions = { + services: { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, + state: {}, + }; + + beforeEach(() => { + // @ts-ignore + Date = FakeDate; + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return [legacyAlert]; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + replaceState.mockReset(); + scheduleActions.mockReset(); + getState.mockReset(); + }); + + it('should fire actions', async () => { + const alert = new KibanaVersionMismatchAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, + ccs: null, + ui: { + isFiring: true, + message: { + text: 'Multiple versions of Kibana ([8.0.0, 7.2.1]) running in this cluster.', + }, + severity: 'warning', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + action: + '[View instances](http://localhost:5601/app/monitoring#kibana/instances?_g=(cluster_uuid:abc123))', + actionPlain: 'Verify you have the same version across all instances.', + internalFullMessage: + 'Kibana version mismatch alert is firing for testCluster. Kibana is running [8.0.0, 7.2.1]. [View instances](http://localhost:5601/app/monitoring#kibana/instances?_g=(cluster_uuid:abc123))', + internalShortMessage: + 'Kibana version mismatch alert is firing for testCluster. Verify you have the same version across all instances.', + versionList: '[8.0.0, 7.2.1]', + clusterName, + state: 'firing', + }); + }); + + it('should not fire actions if there is no legacy alert', async () => { + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return []; + }); + const alert = new KibanaVersionMismatchAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).not.toHaveBeenCalledWith({}); + expect(scheduleActions).not.toHaveBeenCalled(); + }); + + it('should resolve with a resolved message', async () => { + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return [ + { + ...legacyAlert, + resolved_timestamp: 1, + }, + ]; + }); + (getState as jest.Mock).mockImplementation(() => { + return { + alertStates: [ + { + cluster: { + clusterUuid, + clusterName, + }, + ccs: null, + ui: { + isFiring: true, + message: null, + severity: 'danger', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }; + }); + const alert = new KibanaVersionMismatchAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid, clusterName }, + ccs: null, + ui: { + isFiring: false, + message: { + text: 'All versions of Kibana are the same in this cluster.', + }, + severity: 'danger', + resolvedMS: 1, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: 'Kibana version mismatch alert is resolved for testCluster.', + internalShortMessage: 'Kibana version mismatch alert is resolved for testCluster.', + clusterName, + state: 'resolved', + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts new file mode 100644 index 0000000000000..80e8701933f56 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts @@ -0,0 +1,253 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IUiSettingsClient } from 'kibana/server'; +import { i18n } from '@kbn/i18n'; +import { BaseAlert } from './base_alert'; +import { + AlertData, + AlertCluster, + AlertState, + AlertMessage, + AlertInstanceState, + LegacyAlert, +} from './types'; +import { AlertInstance } from '../../../alerts/server'; +import { INDEX_ALERTS, ALERT_KIBANA_VERSION_MISMATCH } from '../../common/constants'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { AlertSeverity } from '../../common/enums'; +import { CommonAlertParams } from '../../common/types'; +import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; + +const WATCH_NAME = 'kibana_version_mismatch'; +const RESOLVED = i18n.translate('xpack.monitoring.alerts.kibanaVersionMismatch.resolved', { + defaultMessage: 'resolved', +}); +const FIRING = i18n.translate('xpack.monitoring.alerts.kibanaVersionMismatch.firing', { + defaultMessage: 'firing', +}); + +export class KibanaVersionMismatchAlert extends BaseAlert { + public type = ALERT_KIBANA_VERSION_MISMATCH; + public label = i18n.translate('xpack.monitoring.alerts.kibanaVersionMismatch.label', { + defaultMessage: 'Kibana version mismatch', + }); + public isLegacy = true; + + protected actionVariables = [ + { + name: 'internalShortMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.actionVariables.internalShortMessage', + { + defaultMessage: 'The short internal message generated by Elastic.', + } + ), + }, + { + name: 'internalFullMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.actionVariables.internalFullMessage', + { + defaultMessage: 'The full internal message generated by Elastic.', + } + ), + }, + { + name: 'state', + description: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.actionVariables.state', + { + defaultMessage: 'The current state of the alert.', + } + ), + }, + { + name: 'versionList', + description: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.actionVariables.clusterHealth', + { + defaultMessage: 'The versions of Kibana running in this cluster.', + } + ), + }, + { + name: 'clusterName', + description: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.actionVariables.clusterName', + { + defaultMessage: 'The cluster to which the instances belong.', + } + ), + }, + { + name: 'action', + description: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.actionVariables.action', + { + defaultMessage: 'The recommended action for this alert.', + } + ), + }, + { + name: 'actionPlain', + description: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.actionVariables.actionPlain', + { + defaultMessage: 'The recommended action for this alert, without any markdown.', + } + ), + }, + ]; + + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + uiSettings: IUiSettingsClient, + availableCcs: string[] + ): Promise { + let alertIndexPattern = INDEX_ALERTS; + if (availableCcs) { + alertIndexPattern = getCcsIndexPattern(alertIndexPattern, availableCcs); + } + const legacyAlerts = await fetchLegacyAlerts( + callCluster, + clusters, + alertIndexPattern, + WATCH_NAME, + this.config.ui.max_bucket_size + ); + + return legacyAlerts.reduce((accum: AlertData[], legacyAlert) => { + const severity = AlertSeverity.Warning; + accum.push({ + instanceKey: `${legacyAlert.metadata.cluster_uuid}`, + clusterUuid: legacyAlert.metadata.cluster_uuid, + shouldFire: !legacyAlert.resolved_timestamp, + severity, + meta: legacyAlert, + ccs: null, + }); + return accum; + }, []); + } + + private getVersions(legacyAlert: LegacyAlert) { + const prefixStr = 'Versions: '; + return legacyAlert.message.slice( + legacyAlert.message.indexOf(prefixStr) + prefixStr.length, + legacyAlert.message.length - 1 + ); + } + + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { + const legacyAlert = item.meta as LegacyAlert; + const versions = this.getVersions(legacyAlert); + if (!alertState.ui.isFiring) { + return { + text: i18n.translate('xpack.monitoring.alerts.kibanaVersionMismatch.ui.resolvedMessage', { + defaultMessage: `All versions of Kibana are the same in this cluster.`, + }), + }; + } + + const text = i18n.translate('xpack.monitoring.alerts.kibanaVersionMismatch.ui.firingMessage', { + defaultMessage: `Multiple versions of Kibana ({versions}) running in this cluster.`, + values: { + versions, + }, + }); + + return { + text, + }; + } + + protected async executeActions( + instance: AlertInstance, + instanceState: AlertInstanceState, + item: AlertData, + cluster: AlertCluster + ) { + if (instanceState.alertStates.length === 0) { + return; + } + const alertState = instanceState.alertStates[0]; + const legacyAlert = item.meta as LegacyAlert; + const versions = this.getVersions(legacyAlert); + if (!alertState.ui.isFiring) { + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.resolved.internalShortMessage', + { + defaultMessage: `Kibana version mismatch alert is resolved for {clusterName}.`, + values: { + clusterName: cluster.clusterName, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.resolved.internalFullMessage', + { + defaultMessage: `Kibana version mismatch alert is resolved for {clusterName}.`, + values: { + clusterName: cluster.clusterName, + }, + } + ), + state: RESOLVED, + clusterName: cluster.clusterName, + }); + } else { + const shortActionText = i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.shortAction', + { + defaultMessage: 'Verify you have the same version across all instances.', + } + ); + const fullActionText = i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.fullAction', + { + defaultMessage: 'View instances', + } + ); + const globalState = [`cluster_uuid:${cluster.clusterUuid}`]; + if (alertState.ccs) { + globalState.push(`ccs:${alertState.ccs}`); + } + const url = `${this.kibanaUrl}/app/monitoring#kibana/instances?_g=(${globalState.join(',')})`; + const action = `[${fullActionText}](${url})`; + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalShortMessage', + { + defaultMessage: `Kibana version mismatch alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalFullMessage', + { + defaultMessage: `Kibana version mismatch alert is firing for {clusterName}. Kibana is running {versions}. {action}`, + values: { + clusterName: cluster.clusterName, + versions, + action, + }, + } + ), + state: FIRING, + clusterName: cluster.clusterName, + versionList: versions, + action, + actionPlain: shortActionText, + }); + } + } +} diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts deleted file mode 100644 index fb8d10884fdc7..0000000000000 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import moment from 'moment-timezone'; -import { getLicenseExpiration } from './license_expiration'; -import { ALERT_TYPE_LICENSE_EXPIRATION } from '../../common/constants'; -import { Logger } from 'src/core/server'; -import { - AlertCommonParams, - AlertCommonState, - AlertLicensePerClusterState, - AlertLicense, -} from './types'; -import { executeActions } from '../lib/alerts/license_expiration.lib'; -import { PreparedAlert, getPreparedAlert } from '../lib/alerts/get_prepared_alert'; -import { alertsMock, AlertServicesMock } from '../../../alerts/server/mocks'; - -jest.mock('../lib/alerts/license_expiration.lib', () => ({ - executeActions: jest.fn(), - getUiMessage: jest.fn(), -})); - -jest.mock('../lib/alerts/get_prepared_alert', () => ({ - getPreparedAlert: jest.fn(() => { - return { - emailAddress: 'foo@foo.com', - }; - }), -})); - -describe('getLicenseExpiration', () => { - const services: AlertServicesMock = alertsMock.createAlertServices(); - - const params: AlertCommonParams = { - dateFormat: 'YYYY', - timezone: 'UTC', - }; - - const emailAddress = 'foo@foo.com'; - const clusterUuid = 'kdksdfj434'; - const clusterName = 'monitoring_test'; - const dateFormat = 'YYYY-MM-DD'; - const cluster = { clusterUuid, clusterName }; - const defaultUiState = { - isFiring: false, - severity: 0, - message: null, - resolvedMS: 0, - lastCheckedMS: 0, - triggeredMS: 0, - }; - - async function setupAlert( - license: AlertLicense | null, - expiredCheckDateMS: number, - preparedAlertResponse: PreparedAlert | null | undefined = undefined - ): Promise { - const logger: Logger = { - warn: jest.fn(), - log: jest.fn(), - debug: jest.fn(), - trace: jest.fn(), - error: jest.fn(), - fatal: jest.fn(), - info: jest.fn(), - get: jest.fn(), - }; - const getLogger = (): Logger => logger; - const ccrEnabled = false; - (getPreparedAlert as jest.Mock).mockImplementation(() => { - if (preparedAlertResponse !== undefined) { - return preparedAlertResponse; - } - - return { - emailAddress, - data: [license], - clusters: [cluster], - dateFormat, - }; - }); - - const alert = getLicenseExpiration(null as any, null as any, getLogger, ccrEnabled); - const state: AlertCommonState = { - [clusterUuid]: { - expiredCheckDateMS, - ui: { ...defaultUiState }, - } as AlertLicensePerClusterState, - }; - - return (await alert.executor({ services, params, state } as any)) as AlertCommonState; - } - - afterEach(() => { - jest.clearAllMocks(); - (executeActions as jest.Mock).mockClear(); - (getPreparedAlert as jest.Mock).mockClear(); - }); - - it('should have the right id and actionGroups', () => { - const alert = getLicenseExpiration(null as any, null as any, jest.fn(), false); - expect(alert.id).toBe(ALERT_TYPE_LICENSE_EXPIRATION); - expect(alert.actionGroups).toEqual([{ id: 'default', name: 'Default' }]); - }); - - it('should return the state if no license is provided', async () => { - const result = await setupAlert(null, 0, null); - expect(result[clusterUuid].ui).toEqual(defaultUiState); - }); - - it('should fire actions if going to expire', async () => { - const expiryDateMS = moment().add(7, 'days').valueOf(); - const license = { - status: 'active', - type: 'gold', - expiryDateMS, - clusterUuid, - }; - const result = await setupAlert(license, 0); - const newState = result[clusterUuid] as AlertLicensePerClusterState; - expect(newState.expiredCheckDateMS > 0).toBe(true); - expect(executeActions).toHaveBeenCalledWith( - services.alertInstanceFactory(ALERT_TYPE_LICENSE_EXPIRATION), - cluster, - moment.utc(expiryDateMS), - dateFormat, - emailAddress - ); - }); - - it('should fire actions if the user fixed their license', async () => { - const expiryDateMS = moment().add(365, 'days').valueOf(); - const license = { - status: 'active', - type: 'gold', - expiryDateMS, - clusterUuid, - }; - const result = await setupAlert(license, 100); - const newState = result[clusterUuid] as AlertLicensePerClusterState; - expect(newState.expiredCheckDateMS).toBe(0); - expect(executeActions).toHaveBeenCalledWith( - services.alertInstanceFactory(ALERT_TYPE_LICENSE_EXPIRATION), - cluster, - moment.utc(expiryDateMS), - dateFormat, - emailAddress, - true - ); - }); - - it('should not fire actions for trial license that expire in more than 14 days', async () => { - const expiryDateMS = moment().add(20, 'days').valueOf(); - const license = { - status: 'active', - type: 'trial', - expiryDateMS, - clusterUuid, - }; - const result = await setupAlert(license, 0); - const newState = result[clusterUuid] as AlertLicensePerClusterState; - expect(newState.expiredCheckDateMS).toBe(0); - expect(executeActions).not.toHaveBeenCalled(); - }); - - it('should fire actions for trial license that in 14 days or less', async () => { - const expiryDateMS = moment().add(7, 'days').valueOf(); - const license = { - status: 'active', - type: 'trial', - expiryDateMS, - clusterUuid, - }; - const result = await setupAlert(license, 0); - const newState = result[clusterUuid] as AlertLicensePerClusterState; - expect(newState.expiredCheckDateMS > 0).toBe(true); - expect(executeActions).toHaveBeenCalledWith( - services.alertInstanceFactory(ALERT_TYPE_LICENSE_EXPIRATION), - cluster, - moment.utc(expiryDateMS), - dateFormat, - emailAddress - ); - }); -}); diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration.ts deleted file mode 100644 index 277e108e8f0c0..0000000000000 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import moment from 'moment-timezone'; -import { Logger, ILegacyCustomClusterClient, UiSettingsServiceStart } from 'src/core/server'; -import { i18n } from '@kbn/i18n'; -import { ALERT_TYPE_LICENSE_EXPIRATION } from '../../common/constants'; -import { AlertType } from '../../../alerts/server'; -import { fetchLicenses } from '../lib/alerts/fetch_licenses'; -import { - AlertCommonState, - AlertLicensePerClusterState, - AlertCommonExecutorOptions, - AlertCommonCluster, - AlertLicensePerClusterUiState, -} from './types'; -import { executeActions, getUiMessage } from '../lib/alerts/license_expiration.lib'; -import { getPreparedAlert } from '../lib/alerts/get_prepared_alert'; - -const EXPIRES_DAYS = [60, 30, 14, 7]; - -export const getLicenseExpiration = ( - getUiSettingsService: () => Promise, - monitoringCluster: ILegacyCustomClusterClient, - getLogger: (...scopes: string[]) => Logger, - ccsEnabled: boolean -): AlertType => { - const logger = getLogger(ALERT_TYPE_LICENSE_EXPIRATION); - return { - id: ALERT_TYPE_LICENSE_EXPIRATION, - name: 'Monitoring Alert - License Expiration', - actionGroups: [ - { - id: 'default', - name: i18n.translate('xpack.monitoring.alerts.licenseExpiration.actionGroups.default', { - defaultMessage: 'Default', - }), - }, - ], - defaultActionGroupId: 'default', - producer: 'monitoring', - async executor({ services, params, state }: AlertCommonExecutorOptions): Promise { - logger.debug( - `Firing alert with params: ${JSON.stringify(params)} and state: ${JSON.stringify(state)}` - ); - - const preparedAlert = await getPreparedAlert( - ALERT_TYPE_LICENSE_EXPIRATION, - getUiSettingsService, - monitoringCluster, - logger, - ccsEnabled, - services, - fetchLicenses - ); - - if (!preparedAlert) { - return state; - } - - const { emailAddress, data: licenses, clusters, dateFormat } = preparedAlert; - - const result: AlertCommonState = { ...state }; - const defaultAlertState: AlertLicensePerClusterState = { - expiredCheckDateMS: 0, - ui: { - isFiring: false, - message: null, - severity: 0, - resolvedMS: 0, - lastCheckedMS: 0, - triggeredMS: 0, - }, - }; - - for (const license of licenses) { - const alertState: AlertLicensePerClusterState = - (state[license.clusterUuid] as AlertLicensePerClusterState) || defaultAlertState; - const cluster = clusters.find( - (c: AlertCommonCluster) => c.clusterUuid === license.clusterUuid - ); - if (!cluster) { - logger.warn(`Unable to find cluster for clusterUuid='${license.clusterUuid}'`); - continue; - } - const $expiry = moment.utc(license.expiryDateMS); - let isExpired = false; - let severity = 0; - - if (license.status !== 'active') { - isExpired = true; - severity = 2001; - } else if (license.expiryDateMS) { - for (let i = EXPIRES_DAYS.length - 1; i >= 0; i--) { - if (license.type === 'trial' && i < 2) { - break; - } - - const $fromNow = moment.utc().add(EXPIRES_DAYS[i], 'days'); - if ($fromNow.isAfter($expiry)) { - isExpired = true; - severity = 1000 * i; - break; - } - } - } - - const ui = alertState.ui; - let triggered = ui.triggeredMS; - let resolved = ui.resolvedMS; - let message = ui.message; - let expiredCheckDate = alertState.expiredCheckDateMS; - const instance = services.alertInstanceFactory(ALERT_TYPE_LICENSE_EXPIRATION); - - if (isExpired) { - if (!alertState.expiredCheckDateMS) { - logger.debug(`License will expire soon, sending email`); - executeActions(instance, cluster, $expiry, dateFormat, emailAddress); - expiredCheckDate = triggered = moment().valueOf(); - } - message = getUiMessage(); - resolved = 0; - } else if (!isExpired && alertState.expiredCheckDateMS) { - logger.debug(`License expiration has been resolved, sending email`); - executeActions(instance, cluster, $expiry, dateFormat, emailAddress, true); - expiredCheckDate = 0; - message = getUiMessage(true); - resolved = moment().valueOf(); - } - - result[license.clusterUuid] = { - expiredCheckDateMS: expiredCheckDate, - ui: { - message, - expirationTime: license.expiryDateMS, - isFiring: expiredCheckDate > 0, - severity, - resolvedMS: resolved, - triggeredMS: triggered, - lastCheckedMS: moment().valueOf(), - } as AlertLicensePerClusterUiState, - } as AlertLicensePerClusterState; - } - - return result; - }, - }; -}; diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts new file mode 100644 index 0000000000000..09173df1d88b1 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts @@ -0,0 +1,281 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { LicenseExpirationAlert } from './license_expiration_alert'; +import { ALERT_LICENSE_EXPIRATION } from '../../common/constants'; +import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; + +const RealDate = Date; + +jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ + fetchLegacyAlerts: jest.fn(), +})); +jest.mock('../lib/alerts/fetch_clusters', () => ({ + fetchClusters: jest.fn(), +})); +jest.mock('moment', () => { + return function () { + return { + format: () => 'THE_DATE', + }; + }; +}); + +describe('LicenseExpirationAlert', () => { + it('should have defaults', () => { + const alert = new LicenseExpirationAlert(); + expect(alert.type).toBe(ALERT_LICENSE_EXPIRATION); + expect(alert.label).toBe('License expiration'); + expect(alert.defaultThrottle).toBe('1m'); + // @ts-ignore + expect(alert.actionVariables).toStrictEqual([ + { + name: 'internalShortMessage', + description: 'The short internal message generated by Elastic.', + }, + { + name: 'internalFullMessage', + description: 'The full internal message generated by Elastic.', + }, + { name: 'state', description: 'The current state of the alert.' }, + { name: 'expiredDate', description: 'The date when the license expires.' }, + + { name: 'clusterName', description: 'The cluster to which the license belong.' }, + { name: 'action', description: 'The recommended action for this alert.' }, + { + name: 'actionPlain', + description: 'The recommended action for this alert, without any markdown.', + }, + ]); + }); + + describe('execute', () => { + function FakeDate() {} + FakeDate.prototype.valueOf = () => 1; + + const clusterUuid = 'abc123'; + const clusterName = 'testCluster'; + const legacyAlert = { + prefix: + 'The license for this cluster expires in {{#relativeTime}}metadata.time{{/relativeTime}} at {{#absoluteTime}}metadata.time{{/absoluteTime}}.', + message: 'Update your license.', + metadata: { + severity: 1000, + cluster_uuid: clusterUuid, + time: 1, + }, + }; + const getUiSettingsService = () => ({ + asScopedToClient: jest.fn(), + }); + const getLogger = () => ({ + debug: jest.fn(), + }); + const monitoringCluster = null; + const config = { + ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + }; + const kibanaUrl = 'http://localhost:5601'; + + const replaceState = jest.fn(); + const scheduleActions = jest.fn(); + const getState = jest.fn(); + const executorOptions = { + services: { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, + state: {}, + }; + + beforeEach(() => { + // @ts-ignore + Date = FakeDate; + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return [legacyAlert]; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + replaceState.mockReset(); + scheduleActions.mockReset(); + getState.mockReset(); + }); + + it('should fire actions', async () => { + const alert = new LicenseExpirationAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid, clusterName }, + ccs: null, + ui: { + isFiring: true, + message: { + text: + 'The license for this cluster expires in #relative at #absolute. #start_linkPlease update your license.#end_link', + tokens: [ + { + startToken: '#relative', + type: 'time', + isRelative: true, + isAbsolute: false, + timestamp: 1, + }, + { + startToken: '#absolute', + type: 'time', + isAbsolute: true, + isRelative: false, + timestamp: 1, + }, + { + startToken: '#start_link', + endToken: '#end_link', + type: 'link', + url: 'license', + }, + ], + }, + severity: 'warning', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + action: + '[Please update your license.](http://localhost:5601/app/monitoring#elasticsearch/nodes?_g=(cluster_uuid:abc123))', + actionPlain: 'Please update your license.', + internalFullMessage: + 'License expiration alert is firing for testCluster. Your license expires in THE_DATE. [Please update your license.](http://localhost:5601/app/monitoring#elasticsearch/nodes?_g=(cluster_uuid:abc123))', + internalShortMessage: + 'License expiration alert is firing for testCluster. Your license expires in THE_DATE. Please update your license.', + clusterName, + expiredDate: 'THE_DATE', + state: 'firing', + }); + }); + + it('should not fire actions if there is no legacy alert', async () => { + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return []; + }); + const alert = new LicenseExpirationAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).not.toHaveBeenCalledWith({}); + expect(scheduleActions).not.toHaveBeenCalled(); + }); + + it('should resolve with a resolved message', async () => { + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return [ + { + ...legacyAlert, + resolved_timestamp: 1, + }, + ]; + }); + (getState as jest.Mock).mockImplementation(() => { + return { + alertStates: [ + { + cluster: { + clusterUuid, + clusterName, + }, + ccs: null, + ui: { + isFiring: true, + message: null, + severity: 'danger', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }; + }); + const alert = new LicenseExpirationAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid, clusterName }, + ccs: null, + ui: { + isFiring: false, + message: { + text: 'The license for this cluster is active.', + }, + severity: 'danger', + resolvedMS: 1, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: 'License expiration alert is resolved for testCluster.', + internalShortMessage: 'License expiration alert is resolved for testCluster.', + clusterName, + expiredDate: 'THE_DATE', + state: 'resolved', + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts new file mode 100644 index 0000000000000..7a249db28d2db --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts @@ -0,0 +1,262 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import moment from 'moment'; +import { IUiSettingsClient } from 'kibana/server'; +import { i18n } from '@kbn/i18n'; +import { BaseAlert } from './base_alert'; +import { + AlertData, + AlertCluster, + AlertState, + AlertMessage, + AlertMessageTimeToken, + AlertMessageLinkToken, + AlertInstanceState, + LegacyAlert, +} from './types'; +import { AlertInstance } from '../../../alerts/server'; +import { + INDEX_ALERTS, + ALERT_LICENSE_EXPIRATION, + FORMAT_DURATION_TEMPLATE_SHORT, +} from '../../common/constants'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { AlertMessageTokenType } from '../../common/enums'; +import { CommonAlertParams } from '../../common/types'; +import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { mapLegacySeverity } from '../lib/alerts/map_legacy_severity'; + +const RESOLVED = i18n.translate('xpack.monitoring.alerts.licenseExpiration.resolved', { + defaultMessage: 'resolved', +}); +const FIRING = i18n.translate('xpack.monitoring.alerts.licenseExpiration.firing', { + defaultMessage: 'firing', +}); + +const WATCH_NAME = 'xpack_license_expiration'; + +export class LicenseExpirationAlert extends BaseAlert { + public type = ALERT_LICENSE_EXPIRATION; + public label = i18n.translate('xpack.monitoring.alerts.licenseExpiration.label', { + defaultMessage: 'License expiration', + }); + public isLegacy = true; + protected actionVariables = [ + { + name: 'internalShortMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.actionVariables.internalShortMessage', + { + defaultMessage: 'The short internal message generated by Elastic.', + } + ), + }, + { + name: 'internalFullMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.actionVariables.internalFullMessage', + { + defaultMessage: 'The full internal message generated by Elastic.', + } + ), + }, + { + name: 'state', + description: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.actionVariables.state', + { + defaultMessage: 'The current state of the alert.', + } + ), + }, + { + name: 'expiredDate', + description: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.actionVariables.expiredDate', + { + defaultMessage: 'The date when the license expires.', + } + ), + }, + { + name: 'clusterName', + description: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.actionVariables.clusterName', + { + defaultMessage: 'The cluster to which the license belong.', + } + ), + }, + { + name: 'action', + description: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.actionVariables.action', + { + defaultMessage: 'The recommended action for this alert.', + } + ), + }, + { + name: 'actionPlain', + description: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.actionVariables.actionPlain', + { + defaultMessage: 'The recommended action for this alert, without any markdown.', + } + ), + }, + ]; + + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + uiSettings: IUiSettingsClient, + availableCcs: string[] + ): Promise { + let alertIndexPattern = INDEX_ALERTS; + if (availableCcs) { + alertIndexPattern = getCcsIndexPattern(alertIndexPattern, availableCcs); + } + const legacyAlerts = await fetchLegacyAlerts( + callCluster, + clusters, + alertIndexPattern, + WATCH_NAME, + this.config.ui.max_bucket_size + ); + return legacyAlerts.reduce((accum: AlertData[], legacyAlert) => { + accum.push({ + instanceKey: `${legacyAlert.metadata.cluster_uuid}`, + clusterUuid: legacyAlert.metadata.cluster_uuid, + shouldFire: !legacyAlert.resolved_timestamp, + severity: mapLegacySeverity(legacyAlert.metadata.severity), + meta: legacyAlert, + ccs: null, + }); + return accum; + }, []); + } + + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { + const legacyAlert = item.meta as LegacyAlert; + if (!alertState.ui.isFiring) { + return { + text: i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.resolvedMessage', { + defaultMessage: `The license for this cluster is active.`, + }), + }; + } + return { + text: i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.firingMessage', { + defaultMessage: `The license for this cluster expires in #relative at #absolute. #start_linkPlease update your license.#end_link`, + }), + tokens: [ + { + startToken: '#relative', + type: AlertMessageTokenType.Time, + isRelative: true, + isAbsolute: false, + timestamp: legacyAlert.metadata.time, + } as AlertMessageTimeToken, + { + startToken: '#absolute', + type: AlertMessageTokenType.Time, + isAbsolute: true, + isRelative: false, + timestamp: legacyAlert.metadata.time, + } as AlertMessageTimeToken, + { + startToken: '#start_link', + endToken: '#end_link', + type: AlertMessageTokenType.Link, + url: 'license', + } as AlertMessageLinkToken, + ], + }; + } + + protected async executeActions( + instance: AlertInstance, + instanceState: AlertInstanceState, + item: AlertData, + cluster: AlertCluster + ) { + if (instanceState.alertStates.length === 0) { + return; + } + const alertState = instanceState.alertStates[0]; + const legacyAlert = item.meta as LegacyAlert; + const $expiry = moment(legacyAlert.metadata.time); + if (!alertState.ui.isFiring) { + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.resolved.internalShortMessage', + { + defaultMessage: `License expiration alert is resolved for {clusterName}.`, + values: { + clusterName: cluster.clusterName, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.resolved.internalFullMessage', + { + defaultMessage: `License expiration alert is resolved for {clusterName}.`, + values: { + clusterName: cluster.clusterName, + }, + } + ), + state: RESOLVED, + expiredDate: $expiry.format(FORMAT_DURATION_TEMPLATE_SHORT).trim(), + clusterName: cluster.clusterName, + }); + } else { + const actionText = i18n.translate('xpack.monitoring.alerts.licenseExpiration.action', { + defaultMessage: 'Please update your license.', + }); + const globalState = [`cluster_uuid:${cluster.clusterUuid}`]; + if (alertState.ccs) { + globalState.push(`ccs:${alertState.ccs}`); + } + const url = `${this.kibanaUrl}/app/monitoring#elasticsearch/nodes?_g=(${globalState.join( + ',' + )})`; + const action = `[${actionText}](${url})`; + const expiredDate = $expiry.format(FORMAT_DURATION_TEMPLATE_SHORT).trim(); + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.firing.internalShortMessage', + { + defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {actionText}`, + values: { + clusterName: cluster.clusterName, + expiredDate, + actionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.firing.internalFullMessage', + { + defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {action}`, + values: { + clusterName: cluster.clusterName, + expiredDate, + action, + }, + } + ), + state: FIRING, + expiredDate, + clusterName: cluster.clusterName, + action, + actionPlain: actionText, + }); + } + } +} diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts new file mode 100644 index 0000000000000..3f6d38809a949 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts @@ -0,0 +1,250 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { LogstashVersionMismatchAlert } from './logstash_version_mismatch_alert'; +import { ALERT_LOGSTASH_VERSION_MISMATCH } from '../../common/constants'; +import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; + +const RealDate = Date; + +jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ + fetchLegacyAlerts: jest.fn(), +})); +jest.mock('../lib/alerts/fetch_clusters', () => ({ + fetchClusters: jest.fn(), +})); + +describe('LogstashVersionMismatchAlert', () => { + it('should have defaults', () => { + const alert = new LogstashVersionMismatchAlert(); + expect(alert.type).toBe(ALERT_LOGSTASH_VERSION_MISMATCH); + expect(alert.label).toBe('Logstash version mismatch'); + expect(alert.defaultThrottle).toBe('1m'); + // @ts-ignore + expect(alert.actionVariables).toStrictEqual([ + { + name: 'internalShortMessage', + description: 'The short internal message generated by Elastic.', + }, + { + name: 'internalFullMessage', + description: 'The full internal message generated by Elastic.', + }, + { name: 'state', description: 'The current state of the alert.' }, + { + name: 'versionList', + description: 'The versions of Logstash running in this cluster.', + }, + { name: 'clusterName', description: 'The cluster to which the nodes belong.' }, + { name: 'action', description: 'The recommended action for this alert.' }, + { + name: 'actionPlain', + description: 'The recommended action for this alert, without any markdown.', + }, + ]); + }); + + describe('execute', () => { + function FakeDate() {} + FakeDate.prototype.valueOf = () => 1; + + const clusterUuid = 'abc123'; + const clusterName = 'testCluster'; + const legacyAlert = { + prefix: 'This cluster is running with multiple versions of Logstash.', + message: 'Versions: [8.0.0, 7.2.1].', + metadata: { + severity: 1000, + cluster_uuid: clusterUuid, + }, + }; + const getUiSettingsService = () => ({ + asScopedToClient: jest.fn(), + }); + const getLogger = () => ({ + debug: jest.fn(), + }); + const monitoringCluster = null; + const config = { + ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + }; + const kibanaUrl = 'http://localhost:5601'; + + const replaceState = jest.fn(); + const scheduleActions = jest.fn(); + const getState = jest.fn(); + const executorOptions = { + services: { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, + state: {}, + }; + + beforeEach(() => { + // @ts-ignore + Date = FakeDate; + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return [legacyAlert]; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + replaceState.mockReset(); + scheduleActions.mockReset(); + getState.mockReset(); + }); + + it('should fire actions', async () => { + const alert = new LogstashVersionMismatchAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, + ccs: null, + ui: { + isFiring: true, + message: { + text: 'Multiple versions of Logstash ([8.0.0, 7.2.1]) running in this cluster.', + }, + severity: 'warning', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + action: + '[View nodes](http://localhost:5601/app/monitoring#logstash/nodes?_g=(cluster_uuid:abc123))', + actionPlain: 'Verify you have the same version across all nodes.', + internalFullMessage: + 'Logstash version mismatch alert is firing for testCluster. Logstash is running [8.0.0, 7.2.1]. [View nodes](http://localhost:5601/app/monitoring#logstash/nodes?_g=(cluster_uuid:abc123))', + internalShortMessage: + 'Logstash version mismatch alert is firing for testCluster. Verify you have the same version across all nodes.', + versionList: '[8.0.0, 7.2.1]', + clusterName, + state: 'firing', + }); + }); + + it('should not fire actions if there is no legacy alert', async () => { + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return []; + }); + const alert = new LogstashVersionMismatchAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).not.toHaveBeenCalledWith({}); + expect(scheduleActions).not.toHaveBeenCalled(); + }); + + it('should resolve with a resolved message', async () => { + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return [ + { + ...legacyAlert, + resolved_timestamp: 1, + }, + ]; + }); + (getState as jest.Mock).mockImplementation(() => { + return { + alertStates: [ + { + cluster: { + clusterUuid, + clusterName, + }, + ccs: null, + ui: { + isFiring: true, + message: null, + severity: 'danger', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }; + }); + const alert = new LogstashVersionMismatchAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid, clusterName }, + ccs: null, + ui: { + isFiring: false, + message: { + text: 'All versions of Logstash are the same in this cluster.', + }, + severity: 'danger', + resolvedMS: 1, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: 'Logstash version mismatch alert is resolved for testCluster.', + internalShortMessage: 'Logstash version mismatch alert is resolved for testCluster.', + clusterName, + state: 'resolved', + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts new file mode 100644 index 0000000000000..f996e54de28ef --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts @@ -0,0 +1,257 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IUiSettingsClient } from 'kibana/server'; +import { i18n } from '@kbn/i18n'; +import { BaseAlert } from './base_alert'; +import { + AlertData, + AlertCluster, + AlertState, + AlertMessage, + AlertInstanceState, + LegacyAlert, +} from './types'; +import { AlertInstance } from '../../../alerts/server'; +import { INDEX_ALERTS, ALERT_LOGSTASH_VERSION_MISMATCH } from '../../common/constants'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { AlertSeverity } from '../../common/enums'; +import { CommonAlertParams } from '../../common/types'; +import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; + +const WATCH_NAME = 'logstash_version_mismatch'; +const RESOLVED = i18n.translate('xpack.monitoring.alerts.logstashVersionMismatch.resolved', { + defaultMessage: 'resolved', +}); +const FIRING = i18n.translate('xpack.monitoring.alerts.logstashVersionMismatch.firing', { + defaultMessage: 'firing', +}); + +export class LogstashVersionMismatchAlert extends BaseAlert { + public type = ALERT_LOGSTASH_VERSION_MISMATCH; + public label = i18n.translate('xpack.monitoring.alerts.logstashVersionMismatch.label', { + defaultMessage: 'Logstash version mismatch', + }); + public isLegacy = true; + + protected actionVariables = [ + { + name: 'internalShortMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.actionVariables.internalShortMessage', + { + defaultMessage: 'The short internal message generated by Elastic.', + } + ), + }, + { + name: 'internalFullMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.actionVariables.internalFullMessage', + { + defaultMessage: 'The full internal message generated by Elastic.', + } + ), + }, + { + name: 'state', + description: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.actionVariables.state', + { + defaultMessage: 'The current state of the alert.', + } + ), + }, + { + name: 'versionList', + description: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.actionVariables.clusterHealth', + { + defaultMessage: 'The versions of Logstash running in this cluster.', + } + ), + }, + { + name: 'clusterName', + description: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.actionVariables.clusterName', + { + defaultMessage: 'The cluster to which the nodes belong.', + } + ), + }, + { + name: 'action', + description: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.actionVariables.action', + { + defaultMessage: 'The recommended action for this alert.', + } + ), + }, + { + name: 'actionPlain', + description: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.actionVariables.actionPlain', + { + defaultMessage: 'The recommended action for this alert, without any markdown.', + } + ), + }, + ]; + + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + uiSettings: IUiSettingsClient, + availableCcs: string[] + ): Promise { + let alertIndexPattern = INDEX_ALERTS; + if (availableCcs) { + alertIndexPattern = getCcsIndexPattern(alertIndexPattern, availableCcs); + } + const legacyAlerts = await fetchLegacyAlerts( + callCluster, + clusters, + alertIndexPattern, + WATCH_NAME, + this.config.ui.max_bucket_size + ); + + return legacyAlerts.reduce((accum: AlertData[], legacyAlert) => { + const severity = AlertSeverity.Warning; + + accum.push({ + instanceKey: `${legacyAlert.metadata.cluster_uuid}`, + clusterUuid: legacyAlert.metadata.cluster_uuid, + shouldFire: !legacyAlert.resolved_timestamp, + severity, + meta: legacyAlert, + ccs: null, + }); + return accum; + }, []); + } + + private getVersions(legacyAlert: LegacyAlert) { + const prefixStr = 'Versions: '; + return legacyAlert.message.slice( + legacyAlert.message.indexOf(prefixStr) + prefixStr.length, + legacyAlert.message.length - 1 + ); + } + + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { + const legacyAlert = item.meta as LegacyAlert; + const versions = this.getVersions(legacyAlert); + if (!alertState.ui.isFiring) { + return { + text: i18n.translate('xpack.monitoring.alerts.logstashVersionMismatch.ui.resolvedMessage', { + defaultMessage: `All versions of Logstash are the same in this cluster.`, + }), + }; + } + + const text = i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.ui.firingMessage', + { + defaultMessage: `Multiple versions of Logstash ({versions}) running in this cluster.`, + values: { + versions, + }, + } + ); + + return { + text, + }; + } + + protected async executeActions( + instance: AlertInstance, + instanceState: AlertInstanceState, + item: AlertData, + cluster: AlertCluster + ) { + if (instanceState.alertStates.length === 0) { + return; + } + const alertState = instanceState.alertStates[0]; + const legacyAlert = item.meta as LegacyAlert; + const versions = this.getVersions(legacyAlert); + if (!alertState.ui.isFiring) { + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.resolved.internalShortMessage', + { + defaultMessage: `Logstash version mismatch alert is resolved for {clusterName}.`, + values: { + clusterName: cluster.clusterName, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.resolved.internalFullMessage', + { + defaultMessage: `Logstash version mismatch alert is resolved for {clusterName}.`, + values: { + clusterName: cluster.clusterName, + }, + } + ), + state: RESOLVED, + clusterName: cluster.clusterName, + }); + } else { + const shortActionText = i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.shortAction', + { + defaultMessage: 'Verify you have the same version across all nodes.', + } + ); + const fullActionText = i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.fullAction', + { + defaultMessage: 'View nodes', + } + ); + const globalState = [`cluster_uuid:${cluster.clusterUuid}`]; + if (alertState.ccs) { + globalState.push(`ccs:${alertState.ccs}`); + } + const url = `${this.kibanaUrl}/app/monitoring#logstash/nodes?_g=(${globalState.join(',')})`; + const action = `[${fullActionText}](${url})`; + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalShortMessage', + { + defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalFullMessage', + { + defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. Logstash is running {versions}. {action}`, + values: { + clusterName: cluster.clusterName, + versions, + action, + }, + } + ), + state: FIRING, + clusterName: cluster.clusterName, + versionList: versions, + action, + actionPlain: shortActionText, + }); + } + } +} diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts new file mode 100644 index 0000000000000..13c3dbbbe6e8a --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts @@ -0,0 +1,261 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { NodesChangedAlert } from './nodes_changed_alert'; +import { ALERT_NODES_CHANGED } from '../../common/constants'; +import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; + +const RealDate = Date; + +jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ + fetchLegacyAlerts: jest.fn(), +})); +jest.mock('../lib/alerts/fetch_clusters', () => ({ + fetchClusters: jest.fn(), +})); +jest.mock('moment', () => { + return function () { + return { + format: () => 'THE_DATE', + }; + }; +}); + +describe('NodesChangedAlert', () => { + it('should have defaults', () => { + const alert = new NodesChangedAlert(); + expect(alert.type).toBe(ALERT_NODES_CHANGED); + expect(alert.label).toBe('Nodes changed'); + expect(alert.defaultThrottle).toBe('1m'); + // @ts-ignore + expect(alert.actionVariables).toStrictEqual([ + { + name: 'internalShortMessage', + description: 'The short internal message generated by Elastic.', + }, + { + name: 'internalFullMessage', + description: 'The full internal message generated by Elastic.', + }, + { name: 'state', description: 'The current state of the alert.' }, + { name: 'clusterName', description: 'The cluster to which the nodes belong.' }, + { name: 'added', description: 'The list of nodes added to the cluster.' }, + { name: 'removed', description: 'The list of nodes removed from the cluster.' }, + { name: 'restarted', description: 'The list of nodes restarted in the cluster.' }, + { name: 'action', description: 'The recommended action for this alert.' }, + { + name: 'actionPlain', + description: 'The recommended action for this alert, without any markdown.', + }, + ]); + }); + + describe('execute', () => { + function FakeDate() {} + FakeDate.prototype.valueOf = () => 1; + + const clusterUuid = 'abc123'; + const clusterName = 'testCluster'; + const legacyAlert = { + prefix: 'Elasticsearch cluster nodes have changed!', + message: 'Node was restarted [1]: [test].', + metadata: { + severity: 1000, + cluster_uuid: clusterUuid, + }, + nodes: { + added: {}, + removed: {}, + restarted: { + test: 'test', + }, + }, + }; + const getUiSettingsService = () => ({ + asScopedToClient: jest.fn(), + }); + const getLogger = () => ({ + debug: jest.fn(), + }); + const monitoringCluster = null; + const config = { + ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + }; + const kibanaUrl = 'http://localhost:5601'; + + const replaceState = jest.fn(); + const scheduleActions = jest.fn(); + const getState = jest.fn(); + const executorOptions = { + services: { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, + state: {}, + }; + + beforeEach(() => { + // @ts-ignore + Date = FakeDate; + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return [legacyAlert]; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + replaceState.mockReset(); + scheduleActions.mockReset(); + getState.mockReset(); + }); + + it('should fire actions', async () => { + const alert = new NodesChangedAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid, clusterName }, + ccs: null, + ui: { + isFiring: true, + message: { + text: "Elasticsearch nodes 'test' restarted in this cluster.", + }, + severity: 'warning', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + action: + '[View nodes](http://localhost:5601/app/monitoring#elasticsearch/nodes?_g=(cluster_uuid:abc123))', + actionPlain: 'Verify that you added, removed, or restarted nodes.', + internalFullMessage: + 'Nodes changed alert is firing for testCluster. The following Elasticsearch nodes have been added: removed: restarted:test. [View nodes](http://localhost:5601/app/monitoring#elasticsearch/nodes?_g=(cluster_uuid:abc123))', + internalShortMessage: + 'Nodes changed alert is firing for testCluster. Verify that you added, removed, or restarted nodes.', + added: '', + removed: '', + restarted: 'test', + clusterName, + state: 'firing', + }); + }); + + it('should not fire actions if there is no legacy alert', async () => { + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return []; + }); + const alert = new NodesChangedAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).not.toHaveBeenCalledWith({}); + expect(scheduleActions).not.toHaveBeenCalled(); + }); + + // This doesn't work because this watch is weird where it sets the resolved timestamp right away + // It is not really worth fixing as this watch will go away in 8.0 + // it('should resolve with a resolved message', async () => { + // (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + // return []; + // }); + // (getState as jest.Mock).mockImplementation(() => { + // return { + // alertStates: [ + // { + // cluster: { + // clusterUuid, + // clusterName, + // }, + // ccs: null, + // ui: { + // isFiring: true, + // message: null, + // severity: 'danger', + // resolvedMS: 0, + // triggeredMS: 1, + // lastCheckedMS: 0, + // }, + // }, + // ], + // }; + // }); + // const alert = new NodesChangedAlert(); + // alert.initializeAlertType( + // getUiSettingsService as any, + // monitoringCluster as any, + // getLogger as any, + // config as any, + // kibanaUrl + // ); + // const type = alert.getAlertType(); + // await type.executor({ + // ...executorOptions, + // // @ts-ignore + // params: alert.defaultParams, + // } as any); + // expect(replaceState).toHaveBeenCalledWith({ + // alertStates: [ + // { + // cluster: { clusterUuid, clusterName }, + // ccs: null, + // ui: { + // isFiring: false, + // message: { + // text: "The license for this cluster is active.", + // }, + // severity: 'danger', + // resolvedMS: 1, + // triggeredMS: 1, + // lastCheckedMS: 0, + // }, + // }, + // ], + // }); + // expect(scheduleActions).toHaveBeenCalledWith('default', { + // clusterName, + // expiredDate: 'THE_DATE', + // state: 'resolved', + // }); + // }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts new file mode 100644 index 0000000000000..5b38503c7ece4 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts @@ -0,0 +1,278 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IUiSettingsClient } from 'kibana/server'; +import { i18n } from '@kbn/i18n'; +import { BaseAlert } from './base_alert'; +import { + AlertData, + AlertCluster, + AlertState, + AlertMessage, + AlertInstanceState, + LegacyAlert, + LegacyAlertNodesChangedList, +} from './types'; +import { AlertInstance } from '../../../alerts/server'; +import { INDEX_ALERTS, ALERT_NODES_CHANGED } from '../../common/constants'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { CommonAlertParams } from '../../common/types'; +import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { mapLegacySeverity } from '../lib/alerts/map_legacy_severity'; + +const WATCH_NAME = 'elasticsearch_nodes'; +const RESOLVED = i18n.translate('xpack.monitoring.alerts.nodesChanged.resolved', { + defaultMessage: 'resolved', +}); +const FIRING = i18n.translate('xpack.monitoring.alerts.nodesChanged.firing', { + defaultMessage: 'firing', +}); + +export class NodesChangedAlert extends BaseAlert { + public type = ALERT_NODES_CHANGED; + public label = i18n.translate('xpack.monitoring.alerts.nodesChanged.label', { + defaultMessage: 'Nodes changed', + }); + public isLegacy = true; + + protected actionVariables = [ + { + name: 'internalShortMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.actionVariables.internalShortMessage', + { + defaultMessage: 'The short internal message generated by Elastic.', + } + ), + }, + { + name: 'internalFullMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.actionVariables.internalFullMessage', + { + defaultMessage: 'The full internal message generated by Elastic.', + } + ), + }, + { + name: 'state', + description: i18n.translate('xpack.monitoring.alerts.nodesChanged.actionVariables.state', { + defaultMessage: 'The current state of the alert.', + }), + }, + { + name: 'clusterName', + description: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.actionVariables.clusterName', + { + defaultMessage: 'The cluster to which the nodes belong.', + } + ), + }, + { + name: 'added', + description: i18n.translate('xpack.monitoring.alerts.nodesChanged.actionVariables.added', { + defaultMessage: 'The list of nodes added to the cluster.', + }), + }, + { + name: 'removed', + description: i18n.translate('xpack.monitoring.alerts.nodesChanged.actionVariables.removed', { + defaultMessage: 'The list of nodes removed from the cluster.', + }), + }, + { + name: 'restarted', + description: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.actionVariables.restarted', + { + defaultMessage: 'The list of nodes restarted in the cluster.', + } + ), + }, + { + name: 'action', + description: i18n.translate('xpack.monitoring.alerts.nodesChanged.actionVariables.action', { + defaultMessage: 'The recommended action for this alert.', + }), + }, + { + name: 'actionPlain', + description: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.actionVariables.actionPlain', + { + defaultMessage: 'The recommended action for this alert, without any markdown.', + } + ), + }, + ]; + + private getNodeStates(legacyAlert: LegacyAlert): LegacyAlertNodesChangedList | undefined { + return legacyAlert.nodes; + } + + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + uiSettings: IUiSettingsClient, + availableCcs: string[] + ): Promise { + let alertIndexPattern = INDEX_ALERTS; + if (availableCcs) { + alertIndexPattern = getCcsIndexPattern(alertIndexPattern, availableCcs); + } + const legacyAlerts = await fetchLegacyAlerts( + callCluster, + clusters, + alertIndexPattern, + WATCH_NAME, + this.config.ui.max_bucket_size + ); + return legacyAlerts.reduce((accum: AlertData[], legacyAlert) => { + accum.push({ + instanceKey: `${legacyAlert.metadata.cluster_uuid}`, + clusterUuid: legacyAlert.metadata.cluster_uuid, + shouldFire: true, // This alert always has a resolved timestamp + severity: mapLegacySeverity(legacyAlert.metadata.severity), + meta: legacyAlert, + ccs: null, + }); + return accum; + }, []); + } + + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { + const legacyAlert = item.meta as LegacyAlert; + const states = this.getNodeStates(legacyAlert) || { added: {}, removed: {}, restarted: {} }; + if (!alertState.ui.isFiring) { + return { + text: i18n.translate('xpack.monitoring.alerts.nodesChanged.ui.resolvedMessage', { + defaultMessage: `No changes in Elasticsearch nodes for this cluster.`, + }), + }; + } + + const addedText = + Object.values(states.added).length > 0 + ? i18n.translate('xpack.monitoring.alerts.nodesChanged.ui.addedFiringMessage', { + defaultMessage: `Elasticsearch nodes '{added}' added to this cluster.`, + values: { + added: Object.values(states.added).join(','), + }, + }) + : null; + const removedText = + Object.values(states.removed).length > 0 + ? i18n.translate('xpack.monitoring.alerts.nodesChanged.ui.removedFiringMessage', { + defaultMessage: `Elasticsearch nodes '{removed}' removed from this cluster.`, + values: { + removed: Object.values(states.removed).join(','), + }, + }) + : null; + const restartedText = + Object.values(states.restarted).length > 0 + ? i18n.translate('xpack.monitoring.alerts.nodesChanged.ui.restartedFiringMessage', { + defaultMessage: `Elasticsearch nodes '{restarted}' restarted in this cluster.`, + values: { + restarted: Object.values(states.restarted).join(','), + }, + }) + : null; + + return { + text: [addedText, removedText, restartedText].filter(Boolean).join(' '), + }; + } + + protected async executeActions( + instance: AlertInstance, + instanceState: AlertInstanceState, + item: AlertData, + cluster: AlertCluster + ) { + if (instanceState.alertStates.length === 0) { + return; + } + const alertState = instanceState.alertStates[0]; + const legacyAlert = item.meta as LegacyAlert; + if (!alertState.ui.isFiring) { + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.resolved.internalShortMessage', + { + defaultMessage: `Elasticsearch nodes changed alert is resolved for {clusterName}.`, + values: { + clusterName: cluster.clusterName, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.resolved.internalFullMessage', + { + defaultMessage: `Elasticsearch nodes changed alert is resolved for {clusterName}.`, + values: { + clusterName: cluster.clusterName, + }, + } + ), + state: RESOLVED, + clusterName: cluster.clusterName, + }); + } else { + const shortActionText = i18n.translate('xpack.monitoring.alerts.nodesChanged.shortAction', { + defaultMessage: 'Verify that you added, removed, or restarted nodes.', + }); + const fullActionText = i18n.translate('xpack.monitoring.alerts.nodesChanged.fullAction', { + defaultMessage: 'View nodes', + }); + const globalState = [`cluster_uuid:${cluster.clusterUuid}`]; + if (alertState.ccs) { + globalState.push(`ccs:${alertState.ccs}`); + } + const url = `${this.kibanaUrl}/app/monitoring#elasticsearch/nodes?_g=(${globalState.join( + ',' + )})`; + const action = `[${fullActionText}](${url})`; + const states = this.getNodeStates(legacyAlert) || { added: {}, removed: {}, restarted: {} }; + const added = Object.values(states.added).join(','); + const removed = Object.values(states.removed).join(','); + const restarted = Object.values(states.restarted).join(','); + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.firing.internalShortMessage', + { + defaultMessage: `Nodes changed alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.firing.internalFullMessage', + { + defaultMessage: `Nodes changed alert is firing for {clusterName}. The following Elasticsearch nodes have been added:{added} removed:{removed} restarted:{restarted}. {action}`, + values: { + clusterName: cluster.clusterName, + added, + removed, + restarted, + action, + }, + } + ), + state: FIRING, + clusterName: cluster.clusterName, + added, + removed, + restarted, + action, + actionPlain: shortActionText, + }); + } + } +} diff --git a/x-pack/plugins/monitoring/server/alerts/types.d.ts b/x-pack/plugins/monitoring/server/alerts/types.d.ts index 67c74635b4e36..06988002a2034 100644 --- a/x-pack/plugins/monitoring/server/alerts/types.d.ts +++ b/x-pack/plugins/monitoring/server/alerts/types.d.ts @@ -3,81 +3,106 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Moment } from 'moment'; -import { AlertExecutorOptions } from '../../../alerts/server'; -import { AlertClusterStateState, AlertCommonPerClusterMessageTokenType } from './enums'; - -export interface AlertLicense { - status: string; - type: string; - expiryDateMS: number; - clusterUuid: string; -} - -export interface AlertClusterState { - state: AlertClusterStateState; - clusterUuid: string; -} +import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; -export interface AlertCommonState { - [clusterUuid: string]: AlertCommonPerClusterState; +export interface AlertEnableAction { + id: string; + config: { [key: string]: any }; } -export interface AlertCommonPerClusterState { - ui: AlertCommonPerClusterUiState; +export interface AlertInstanceState { + alertStates: AlertState[]; } -export interface AlertClusterStatePerClusterState extends AlertCommonPerClusterState { - state: AlertClusterStateState; +export interface AlertState { + cluster: AlertCluster; + ccs: string | null; + ui: AlertUiState; } -export interface AlertLicensePerClusterState extends AlertCommonPerClusterState { - expiredCheckDateMS: number; +export interface AlertCpuUsageState extends AlertState { + cpuUsage: number; + nodeId: string; + nodeName: string; } -export interface AlertCommonPerClusterUiState { +export interface AlertUiState { isFiring: boolean; - severity: number; - message: AlertCommonPerClusterMessage | null; + severity: AlertSeverity; + message: AlertMessage | null; resolvedMS: number; lastCheckedMS: number; triggeredMS: number; } -export interface AlertCommonPerClusterMessage { +export interface AlertMessage { text: string; // Do this. #link this is a link #link - tokens?: AlertCommonPerClusterMessageToken[]; + nextSteps?: AlertMessage[]; + tokens?: AlertMessageToken[]; } -export interface AlertCommonPerClusterMessageToken { +export interface AlertMessageToken { startToken: string; endToken?: string; - type: AlertCommonPerClusterMessageTokenType; + type: AlertMessageTokenType; } -export interface AlertCommonPerClusterMessageLinkToken extends AlertCommonPerClusterMessageToken { +export interface AlertMessageLinkToken extends AlertMessageToken { url?: string; } -export interface AlertCommonPerClusterMessageTimeToken extends AlertCommonPerClusterMessageToken { +export interface AlertMessageTimeToken extends AlertMessageToken { isRelative: boolean; isAbsolute: boolean; + timestamp: string | number; } -export interface AlertLicensePerClusterUiState extends AlertCommonPerClusterUiState { - expirationTime: number; +export interface AlertMessageDocLinkToken extends AlertMessageToken { + partialUrl: string; } -export interface AlertCommonCluster { +export interface AlertCluster { clusterUuid: string; clusterName: string; } -export interface AlertCommonExecutorOptions extends AlertExecutorOptions { - state: AlertCommonState; +export interface AlertCpuUsageNodeStats { + clusterUuid: string; + nodeId: string; + nodeName: string; + cpuUsage: number; + containerUsage: number; + containerPeriods: number; + containerQuota: number; + ccs: string | null; +} + +export interface AlertData { + instanceKey: string; + clusterUuid: string; + ccs: string | null; + shouldFire: boolean; + severity: AlertSeverity; + meta: any; +} + +export interface LegacyAlert { + prefix: string; + message: string; + resolved_timestamp: string; + metadata: LegacyAlertMetadata; + nodes?: LegacyAlertNodesChangedList; +} + +export interface LegacyAlertMetadata { + severity: number; + cluster_uuid: string; + time: string; + link: string; } -export interface AlertCommonParams { - dateFormat: string; - timezone: string; +export interface LegacyAlertNodesChangedList { + removed: { [nodeName: string]: string }; + added: { [nodeName: string]: string }; + restarted: { [nodeName: string]: string }; } diff --git a/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.test.ts deleted file mode 100644 index 81e375734cc50..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { executeActions, getUiMessage } from './cluster_state.lib'; -import { AlertClusterStateState } from '../../alerts/enums'; -import { AlertCommonPerClusterMessageLinkToken } from '../../alerts/types'; - -describe('clusterState lib', () => { - describe('executeActions', () => { - const clusterName = 'clusterA'; - const instance: any = { scheduleActions: jest.fn() }; - const license: any = { clusterName }; - const status = AlertClusterStateState.Green; - const emailAddress = 'test@test.com'; - - beforeEach(() => { - instance.scheduleActions.mockClear(); - }); - - it('should schedule actions when firing', () => { - executeActions(instance, license, status, emailAddress, false); - expect(instance.scheduleActions).toHaveBeenCalledWith('default', { - subject: 'NEW X-Pack Monitoring: Cluster Status', - message: `Allocate missing replica shards for cluster '${clusterName}'`, - to: emailAddress, - }); - }); - - it('should have a different message for red state', () => { - executeActions(instance, license, AlertClusterStateState.Red, emailAddress, false); - expect(instance.scheduleActions).toHaveBeenCalledWith('default', { - subject: 'NEW X-Pack Monitoring: Cluster Status', - message: `Allocate missing primary and replica shards for cluster '${clusterName}'`, - to: emailAddress, - }); - }); - - it('should schedule actions when resolved', () => { - executeActions(instance, license, status, emailAddress, true); - expect(instance.scheduleActions).toHaveBeenCalledWith('default', { - subject: 'RESOLVED X-Pack Monitoring: Cluster Status', - message: `This cluster alert has been resolved: Allocate missing replica shards for cluster '${clusterName}'`, - to: emailAddress, - }); - }); - }); - - describe('getUiMessage', () => { - it('should return a message when firing', () => { - const message = getUiMessage(AlertClusterStateState.Red, false); - expect(message.text).toBe( - `Elasticsearch cluster status is red. #start_linkAllocate missing primary and replica shards#end_link` - ); - expect(message.tokens && message.tokens.length).toBe(1); - expect(message.tokens && message.tokens[0].startToken).toBe('#start_link'); - expect(message.tokens && message.tokens[0].endToken).toBe('#end_link'); - expect( - message.tokens && (message.tokens[0] as AlertCommonPerClusterMessageLinkToken).url - ).toBe('elasticsearch/indices'); - }); - - it('should return a message when resolved', () => { - const message = getUiMessage(AlertClusterStateState.Green, true); - expect(message.text).toBe(`Elasticsearch cluster status is green.`); - expect(message.tokens).not.toBeDefined(); - }); - }); -}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.ts b/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.ts deleted file mode 100644 index c4553d87980da..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { i18n } from '@kbn/i18n'; -import { AlertInstance } from '../../../../alerts/server'; -import { - AlertCommonCluster, - AlertCommonPerClusterMessage, - AlertCommonPerClusterMessageLinkToken, -} from '../../alerts/types'; -import { AlertClusterStateState, AlertCommonPerClusterMessageTokenType } from '../../alerts/enums'; - -const RESOLVED_SUBJECT = i18n.translate('xpack.monitoring.alerts.clusterStatus.resolvedSubject', { - defaultMessage: 'RESOLVED X-Pack Monitoring: Cluster Status', -}); - -const NEW_SUBJECT = i18n.translate('xpack.monitoring.alerts.clusterStatus.newSubject', { - defaultMessage: 'NEW X-Pack Monitoring: Cluster Status', -}); - -const RED_STATUS_MESSAGE = i18n.translate('xpack.monitoring.alerts.clusterStatus.redMessage', { - defaultMessage: 'Allocate missing primary and replica shards', -}); - -const YELLOW_STATUS_MESSAGE = i18n.translate( - 'xpack.monitoring.alerts.clusterStatus.yellowMessage', - { - defaultMessage: 'Allocate missing replica shards', - } -); - -export function executeActions( - instance: AlertInstance, - cluster: AlertCommonCluster, - status: AlertClusterStateState, - emailAddress: string, - resolved: boolean = false -) { - const message = - status === AlertClusterStateState.Red ? RED_STATUS_MESSAGE : YELLOW_STATUS_MESSAGE; - if (resolved) { - instance.scheduleActions('default', { - subject: RESOLVED_SUBJECT, - message: `This cluster alert has been resolved: ${message} for cluster '${cluster.clusterName}'`, - to: emailAddress, - }); - } else { - instance.scheduleActions('default', { - subject: NEW_SUBJECT, - message: `${message} for cluster '${cluster.clusterName}'`, - to: emailAddress, - }); - } -} - -export function getUiMessage( - status: AlertClusterStateState, - resolved: boolean = false -): AlertCommonPerClusterMessage { - if (resolved) { - return { - text: i18n.translate('xpack.monitoring.alerts.clusterStatus.ui.resolvedMessage', { - defaultMessage: `Elasticsearch cluster status is green.`, - }), - }; - } - const message = - status === AlertClusterStateState.Red ? RED_STATUS_MESSAGE : YELLOW_STATUS_MESSAGE; - return { - text: i18n.translate('xpack.monitoring.alerts.clusterStatus.ui.firingMessage', { - defaultMessage: `Elasticsearch cluster status is {status}. #start_link{message}#end_link`, - values: { - status, - message, - }, - }), - tokens: [ - { - startToken: '#start_link', - endToken: '#end_link', - type: AlertCommonPerClusterMessageTokenType.Link, - url: 'elasticsearch/indices', - } as AlertCommonPerClusterMessageLinkToken, - ], - }; -} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.test.ts deleted file mode 100644 index 642ae3c39a027..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { fetchClusterState } from './fetch_cluster_state'; - -describe('fetchClusterState', () => { - it('should return the cluster state', async () => { - const status = 'green'; - const clusterUuid = 'sdfdsaj34434'; - const callCluster = jest.fn(() => ({ - hits: { - hits: [ - { - _source: { - cluster_state: { - status, - }, - cluster_uuid: clusterUuid, - }, - }, - ], - }, - })); - - const clusters = [{ clusterUuid, clusterName: 'foo' }]; - const index = '.monitoring-es-*'; - - const state = await fetchClusterState(callCluster, clusters, index); - expect(state).toEqual([ - { - state: status, - clusterUuid, - }, - ]); - }); -}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.ts deleted file mode 100644 index 3fcc3a2c98993..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.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; - * you may not use this file except in compliance with the Elastic License. - */ -import { get } from 'lodash'; -import { AlertCommonCluster, AlertClusterState } from '../../alerts/types'; - -export async function fetchClusterState( - callCluster: any, - clusters: AlertCommonCluster[], - index: string -): Promise { - const params = { - index, - filterPath: ['hits.hits._source.cluster_state.status', 'hits.hits._source.cluster_uuid'], - body: { - size: 1, - sort: [{ timestamp: { order: 'desc' } }], - query: { - bool: { - filter: [ - { - terms: { - cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), - }, - }, - { - term: { - type: 'cluster_stats', - }, - }, - { - range: { - timestamp: { - gte: 'now-2m', - }, - }, - }, - ], - }, - }, - }, - }; - - const response = await callCluster('search', params); - return get(response, 'hits.hits', []).map((hit: any) => { - return { - state: get(hit, '_source.cluster_state.status'), - clusterUuid: get(hit, '_source.cluster_uuid'), - }; - }); -} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts index d1513ac16fb15..48ad31d20a395 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts @@ -4,12 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import { get } from 'lodash'; -import { AlertCommonCluster } from '../../alerts/types'; +import { AlertCluster } from '../../alerts/types'; -export async function fetchClusters( - callCluster: any, - index: string -): Promise { +export async function fetchClusters(callCluster: any, index: string): Promise { const params = { index, filterPath: [ diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts new file mode 100644 index 0000000000000..12926a30efa1b --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts @@ -0,0 +1,228 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { fetchCpuUsageNodeStats } from './fetch_cpu_usage_node_stats'; + +describe('fetchCpuUsageNodeStats', () => { + let callCluster = jest.fn(); + const clusters = [ + { + clusterUuid: 'abc123', + clusterName: 'test', + }, + ]; + const index = '.monitoring-es-*'; + const startMs = 0; + const endMs = 0; + const size = 10; + + it('fetch normal stats', async () => { + callCluster = jest.fn().mockImplementation((...args) => { + return { + aggregations: { + clusters: { + buckets: [ + { + key: clusters[0].clusterUuid, + nodes: { + buckets: [ + { + key: 'theNodeId', + index: { + buckets: [ + { + key: '.monitoring-es-TODAY', + }, + ], + }, + name: { + buckets: [ + { + key: 'theNodeName', + }, + ], + }, + average_cpu: { + value: 10, + }, + }, + ], + }, + }, + ], + }, + }, + }; + }); + const result = await fetchCpuUsageNodeStats(callCluster, clusters, index, startMs, endMs, size); + expect(result).toEqual([ + { + clusterUuid: clusters[0].clusterUuid, + nodeName: 'theNodeName', + nodeId: 'theNodeId', + cpuUsage: 10, + containerUsage: undefined, + containerPeriods: undefined, + containerQuota: undefined, + ccs: null, + }, + ]); + }); + + it('fetch container stats', async () => { + callCluster = jest.fn().mockImplementation((...args) => { + return { + aggregations: { + clusters: { + buckets: [ + { + key: clusters[0].clusterUuid, + nodes: { + buckets: [ + { + key: 'theNodeId', + index: { + buckets: [ + { + key: '.monitoring-es-TODAY', + }, + ], + }, + name: { + buckets: [ + { + key: 'theNodeName', + }, + ], + }, + average_usage: { + value: 10, + }, + average_periods: { + value: 5, + }, + average_quota: { + value: 50, + }, + }, + ], + }, + }, + ], + }, + }, + }; + }); + const result = await fetchCpuUsageNodeStats(callCluster, clusters, index, startMs, endMs, size); + expect(result).toEqual([ + { + clusterUuid: clusters[0].clusterUuid, + nodeName: 'theNodeName', + nodeId: 'theNodeId', + cpuUsage: undefined, + containerUsage: 10, + containerPeriods: 5, + containerQuota: 50, + ccs: null, + }, + ]); + }); + + it('fetch properly return ccs', async () => { + callCluster = jest.fn().mockImplementation((...args) => { + return { + aggregations: { + clusters: { + buckets: [ + { + key: clusters[0].clusterUuid, + nodes: { + buckets: [ + { + key: 'theNodeId', + index: { + buckets: [ + { + key: 'foo:.monitoring-es-TODAY', + }, + ], + }, + name: { + buckets: [ + { + key: 'theNodeName', + }, + ], + }, + average_usage: { + value: 10, + }, + average_periods: { + value: 5, + }, + average_quota: { + value: 50, + }, + }, + ], + }, + }, + ], + }, + }, + }; + }); + const result = await fetchCpuUsageNodeStats(callCluster, clusters, index, startMs, endMs, size); + expect(result[0].ccs).toBe('foo'); + }); + + it('should use consistent params', async () => { + let params = null; + callCluster = jest.fn().mockImplementation((...args) => { + params = args[1]; + }); + await fetchCpuUsageNodeStats(callCluster, clusters, index, startMs, endMs, size); + expect(params).toStrictEqual({ + index, + filterPath: ['aggregations'], + body: { + size: 0, + query: { + bool: { + filter: [ + { terms: { cluster_uuid: clusters.map((cluster) => cluster.clusterUuid) } }, + { term: { type: 'node_stats' } }, + { range: { timestamp: { format: 'epoch_millis', gte: 0, lte: 0 } } }, + ], + }, + }, + aggs: { + clusters: { + terms: { + field: 'cluster_uuid', + size, + include: clusters.map((cluster) => cluster.clusterUuid), + }, + aggs: { + nodes: { + terms: { field: 'node_stats.node_id', size }, + aggs: { + index: { terms: { field: '_index', size: 1 } }, + average_cpu: { avg: { field: 'node_stats.process.cpu.percent' } }, + average_usage: { avg: { field: 'node_stats.os.cgroup.cpuacct.usage_nanos' } }, + average_periods: { + avg: { field: 'node_stats.os.cgroup.cpu.stat.number_of_elapsed_periods' }, + }, + average_quota: { avg: { field: 'node_stats.os.cgroup.cpu.cfs_quota_micros' } }, + name: { terms: { field: 'source_node.name', size: 1 } }, + }, + }, + }, + }, + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts new file mode 100644 index 0000000000000..4fdb03b61950e --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { get } from 'lodash'; +import { AlertCluster, AlertCpuUsageNodeStats } from '../../alerts/types'; + +interface NodeBucketESResponse { + key: string; + average_cpu: { value: number }; +} + +interface ClusterBucketESResponse { + key: string; + nodes: { + buckets: NodeBucketESResponse[]; + }; +} + +export async function fetchCpuUsageNodeStats( + callCluster: any, + clusters: AlertCluster[], + index: string, + startMs: number, + endMs: number, + size: number +): Promise { + const filterPath = ['aggregations']; + const params = { + index, + filterPath, + body: { + size: 0, + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), + }, + }, + { + term: { + type: 'node_stats', + }, + }, + { + range: { + timestamp: { + format: 'epoch_millis', + gte: startMs, + lte: endMs, + }, + }, + }, + ], + }, + }, + aggs: { + clusters: { + terms: { + field: 'cluster_uuid', + size, + include: clusters.map((cluster) => cluster.clusterUuid), + }, + aggs: { + nodes: { + terms: { + field: 'node_stats.node_id', + size, + }, + aggs: { + index: { + terms: { + field: '_index', + size: 1, + }, + }, + average_cpu: { + avg: { + field: 'node_stats.process.cpu.percent', + }, + }, + average_usage: { + avg: { + field: 'node_stats.os.cgroup.cpuacct.usage_nanos', + }, + }, + average_periods: { + avg: { + field: 'node_stats.os.cgroup.cpu.stat.number_of_elapsed_periods', + }, + }, + average_quota: { + avg: { + field: 'node_stats.os.cgroup.cpu.cfs_quota_micros', + }, + }, + name: { + terms: { + field: 'source_node.name', + size: 1, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const response = await callCluster('search', params); + const stats: AlertCpuUsageNodeStats[] = []; + const clusterBuckets = get( + response, + 'aggregations.clusters.buckets', + [] + ) as ClusterBucketESResponse[]; + for (const clusterBucket of clusterBuckets) { + for (const node of clusterBucket.nodes.buckets) { + const indexName = get(node, 'index.buckets[0].key', ''); + stats.push({ + clusterUuid: clusterBucket.key, + nodeId: node.key, + nodeName: get(node, 'name.buckets[0].key'), + cpuUsage: get(node, 'average_cpu.value'), + containerUsage: get(node, 'average_usage.value'), + containerPeriods: get(node, 'average_periods.value'), + containerQuota: get(node, 'average_quota.value'), + ccs: indexName.includes(':') ? indexName.split(':')[0] : null, + }); + } + } + return stats; +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_default_email_address.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_default_email_address.test.ts deleted file mode 100644 index ae914c7a2ace1..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_default_email_address.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { fetchDefaultEmailAddress } from './fetch_default_email_address'; -import { uiSettingsServiceMock } from '../../../../../../src/core/server/mocks'; - -describe('fetchDefaultEmailAddress', () => { - it('get the email address', async () => { - const email = 'test@test.com'; - const uiSettingsClient = uiSettingsServiceMock.createClient(); - uiSettingsClient.get.mockResolvedValue(email); - const result = await fetchDefaultEmailAddress(uiSettingsClient); - expect(result).toBe(email); - }); -}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_default_email_address.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_default_email_address.ts deleted file mode 100644 index 88e4199a88256..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_default_email_address.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { IUiSettingsClient } from 'src/core/server'; -import { MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS } from '../../../common/constants'; - -export async function fetchDefaultEmailAddress( - uiSettingsClient: IUiSettingsClient -): Promise { - return await uiSettingsClient.get(MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS); -} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts new file mode 100644 index 0000000000000..a3743a8ff206f --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { fetchLegacyAlerts } from './fetch_legacy_alerts'; + +describe('fetchLegacyAlerts', () => { + let callCluster = jest.fn(); + const clusters = [ + { + clusterUuid: 'abc123', + clusterName: 'test', + }, + ]; + const index = '.monitoring-es-*'; + const size = 10; + + it('fetch legacy alerts', async () => { + const prefix = 'thePrefix'; + const message = 'theMessage'; + const nodes = {}; + const metadata = { + severity: 2000, + cluster_uuid: clusters[0].clusterUuid, + metadata: {}, + }; + callCluster = jest.fn().mockImplementation(() => { + return { + hits: { + hits: [ + { + _source: { + prefix, + message, + nodes, + metadata, + }, + }, + ], + }, + }; + }); + const result = await fetchLegacyAlerts(callCluster, clusters, index, 'myWatch', size); + expect(result).toEqual([ + { + message, + metadata, + nodes, + prefix, + }, + ]); + }); + + it('should use consistent params', async () => { + let params = null; + callCluster = jest.fn().mockImplementation((...args) => { + params = args[1]; + }); + await fetchLegacyAlerts(callCluster, clusters, index, 'myWatch', size); + expect(params).toStrictEqual({ + index, + filterPath: [ + 'hits.hits._source.prefix', + 'hits.hits._source.message', + 'hits.hits._source.resolved_timestamp', + 'hits.hits._source.nodes', + 'hits.hits._source.metadata.*', + ], + body: { + size, + sort: [{ timestamp: { order: 'desc' } }], + query: { + bool: { + minimum_should_match: 1, + filter: [ + { + terms: { 'metadata.cluster_uuid': clusters.map((cluster) => cluster.clusterUuid) }, + }, + { term: { 'metadata.watch': 'myWatch' } }, + ], + should: [ + { range: { timestamp: { gte: 'now-2m' } } }, + { range: { resolved_timestamp: { gte: 'now-2m' } } }, + { bool: { must_not: { exists: { field: 'resolved_timestamp' } } } }, + ], + }, + }, + collapse: { field: 'metadata.cluster_uuid' }, + }, + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts new file mode 100644 index 0000000000000..fe01a1b921c2e --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { get } from 'lodash'; +import { LegacyAlert, AlertCluster, LegacyAlertMetadata } from '../../alerts/types'; + +export async function fetchLegacyAlerts( + callCluster: any, + clusters: AlertCluster[], + index: string, + watchName: string, + size: number +): Promise { + const params = { + index, + filterPath: [ + 'hits.hits._source.prefix', + 'hits.hits._source.message', + 'hits.hits._source.resolved_timestamp', + 'hits.hits._source.nodes', + 'hits.hits._source.metadata.*', + ], + body: { + size, + sort: [ + { + timestamp: { + order: 'desc', + }, + }, + ], + query: { + bool: { + minimum_should_match: 1, + filter: [ + { + terms: { + 'metadata.cluster_uuid': clusters.map((cluster) => cluster.clusterUuid), + }, + }, + { + term: { + 'metadata.watch': watchName, + }, + }, + ], + should: [ + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + { + range: { + resolved_timestamp: { + gte: 'now-2m', + }, + }, + }, + { + bool: { + must_not: { + exists: { + field: 'resolved_timestamp', + }, + }, + }, + }, + ], + }, + }, + collapse: { + field: 'metadata.cluster_uuid', + }, + }, + }; + + const response = await callCluster('search', params); + return get(response, 'hits.hits', []).map((hit: any) => { + const legacyAlert: LegacyAlert = { + prefix: get(hit, '_source.prefix'), + message: get(hit, '_source.message'), + resolved_timestamp: get(hit, '_source.resolved_timestamp'), + nodes: get(hit, '_source.nodes'), + metadata: get(hit, '_source.metadata') as LegacyAlertMetadata, + }; + return legacyAlert; + }); +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts deleted file mode 100644 index 9dcb4ffb82a5f..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { fetchLicenses } from './fetch_licenses'; - -describe('fetchLicenses', () => { - const clusterName = 'MyCluster'; - const clusterUuid = 'clusterA'; - const license = { - status: 'active', - expiry_date_in_millis: 1579532493876, - type: 'basic', - }; - - it('return a list of licenses', async () => { - const callCluster = jest.fn().mockImplementation(() => ({ - hits: { - hits: [ - { - _source: { - license, - cluster_uuid: clusterUuid, - }, - }, - ], - }, - })); - const clusters = [{ clusterUuid, clusterName }]; - const index = '.monitoring-es-*'; - const result = await fetchLicenses(callCluster, clusters, index); - expect(result).toEqual([ - { - status: license.status, - type: license.type, - expiryDateMS: license.expiry_date_in_millis, - clusterUuid, - }, - ]); - }); - - it('should only search for the clusters provided', async () => { - const callCluster = jest.fn(); - const clusters = [{ clusterUuid, clusterName }]; - const index = '.monitoring-es-*'; - await fetchLicenses(callCluster, clusters, index); - const params = callCluster.mock.calls[0][1]; - expect(params.body.query.bool.filter[0].terms.cluster_uuid).toEqual([clusterUuid]); - }); - - it('should limit the time period in the query', async () => { - const callCluster = jest.fn(); - const clusters = [{ clusterUuid, clusterName }]; - const index = '.monitoring-es-*'; - await fetchLicenses(callCluster, clusters, index); - const params = callCluster.mock.calls[0][1]; - expect(params.body.query.bool.filter[2].range.timestamp.gte).toBe('now-2m'); - }); -}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts deleted file mode 100644 index a65cba493dab9..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { get } from 'lodash'; -import { AlertLicense, AlertCommonCluster } from '../../alerts/types'; - -export async function fetchLicenses( - callCluster: any, - clusters: AlertCommonCluster[], - index: string -): Promise { - const params = { - index, - filterPath: ['hits.hits._source.license.*', 'hits.hits._source.cluster_uuid'], - body: { - size: 1, - sort: [{ timestamp: { order: 'desc' } }], - query: { - bool: { - filter: [ - { - terms: { - cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), - }, - }, - { - term: { - type: 'cluster_stats', - }, - }, - { - range: { - timestamp: { - gte: 'now-2m', - }, - }, - }, - ], - }, - }, - }, - }; - - const response = await callCluster('search', params); - return get(response, 'hits.hits', []).map((hit: any) => { - const rawLicense: any = get(hit, '_source.license', {}); - const license: AlertLicense = { - status: rawLicense.status, - type: rawLicense.type, - expiryDateMS: rawLicense.expiry_date_in_millis, - clusterUuid: get(hit, '_source.cluster_uuid'), - }; - return license; - }); -} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts index a3bcb61afacd6..ff674195f0730 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts @@ -5,22 +5,31 @@ */ import { fetchStatus } from './fetch_status'; -import { AlertCommonPerClusterState } from '../../alerts/types'; +import { AlertUiState, AlertState } from '../../alerts/types'; +import { AlertSeverity } from '../../../common/enums'; +import { ALERT_CPU_USAGE, ALERT_CLUSTER_HEALTH } from '../../../common/constants'; describe('fetchStatus', () => { - const alertType = 'monitoringTest'; + const alertType = ALERT_CPU_USAGE; + const alertTypes = [alertType]; const log = { warn: jest.fn() }; const start = 0; const end = 0; const id = 1; - const defaultUiState = { + const defaultClusterState = { + clusterUuid: 'abc', + clusterName: 'test', + }; + const defaultUiState: AlertUiState = { isFiring: false, - severity: 0, + severity: AlertSeverity.Success, message: null, resolvedMS: 0, lastCheckedMS: 0, triggeredMS: 0, }; + let alertStates: AlertState[] = []; + const licenseService = null; const alertsClient = { find: jest.fn(() => ({ total: 1, @@ -31,10 +40,12 @@ describe('fetchStatus', () => { ], })), getAlertState: jest.fn(() => ({ - alertTypeState: { - state: { - ui: defaultUiState, - } as AlertCommonPerClusterState, + alertInstances: { + abc: { + state: { + alertStates, + }, + }, }, })), }; @@ -45,57 +56,96 @@ describe('fetchStatus', () => { }); it('should fetch from the alerts client', async () => { - const status = await fetchStatus(alertsClient as any, [alertType], start, end, log as any); - expect(status).toEqual([]); + const status = await fetchStatus( + alertsClient as any, + licenseService as any, + alertTypes, + defaultClusterState.clusterUuid, + start, + end, + log as any + ); + expect(status).toEqual({ + monitoring_alert_cpu_usage: { + alert: { + isLegacy: false, + label: 'CPU Usage', + paramDetails: {}, + rawAlert: { id: 1 }, + type: 'monitoring_alert_cpu_usage', + }, + enabled: true, + exists: true, + states: [], + }, + }); }); it('should return alerts that are firing', async () => { - alertsClient.getAlertState = jest.fn(() => ({ - alertTypeState: { - state: { - ui: { - ...defaultUiState, - isFiring: true, - }, - } as AlertCommonPerClusterState, + alertStates = [ + { + cluster: defaultClusterState, + ccs: null, + ui: { + ...defaultUiState, + isFiring: true, + }, }, - })); + ]; - const status = await fetchStatus(alertsClient as any, [alertType], start, end, log as any); - expect(status.length).toBe(1); - expect(status[0].type).toBe(alertType); - expect(status[0].isFiring).toBe(true); + const status = await fetchStatus( + alertsClient as any, + licenseService as any, + alertTypes, + defaultClusterState.clusterUuid, + start, + end, + log as any + ); + expect(Object.values(status).length).toBe(1); + expect(Object.keys(status)).toEqual(alertTypes); + expect(status[alertType].states[0].state.ui.isFiring).toBe(true); }); it('should return alerts that have been resolved in the time period', async () => { - alertsClient.getAlertState = jest.fn(() => ({ - alertTypeState: { - state: { - ui: { - ...defaultUiState, - resolvedMS: 1500, - }, - } as AlertCommonPerClusterState, + alertStates = [ + { + cluster: defaultClusterState, + ccs: null, + ui: { + ...defaultUiState, + resolvedMS: 1500, + }, }, - })); + ]; const customStart = 1000; const customEnd = 2000; const status = await fetchStatus( alertsClient as any, - [alertType], + licenseService as any, + alertTypes, + defaultClusterState.clusterUuid, customStart, customEnd, log as any ); - expect(status.length).toBe(1); - expect(status[0].type).toBe(alertType); - expect(status[0].isFiring).toBe(false); + expect(Object.values(status).length).toBe(1); + expect(Object.keys(status)).toEqual(alertTypes); + expect(status[alertType].states[0].state.ui.isFiring).toBe(false); }); it('should pass in the right filter to the alerts client', async () => { - await fetchStatus(alertsClient as any, [alertType], start, end, log as any); + await fetchStatus( + alertsClient as any, + licenseService as any, + alertTypes, + defaultClusterState.clusterUuid, + start, + end, + log as any + ); expect((alertsClient.find as jest.Mock).mock.calls[0][0].options.filter).toBe( `alert.attributes.alertTypeId:${alertType}` ); @@ -106,8 +156,16 @@ describe('fetchStatus', () => { alertTypeState: null, })) as any; - const status = await fetchStatus(alertsClient as any, [alertType], start, end, log as any); - expect(status).toEqual([]); + const status = await fetchStatus( + alertsClient as any, + licenseService as any, + alertTypes, + defaultClusterState.clusterUuid, + start, + end, + log as any + ); + expect(status[alertType].states.length).toEqual(0); }); it('should return nothing if no alerts are found', async () => { @@ -116,7 +174,34 @@ describe('fetchStatus', () => { data: [], })) as any; - const status = await fetchStatus(alertsClient as any, [alertType], start, end, log as any); - expect(status).toEqual([]); + const status = await fetchStatus( + alertsClient as any, + licenseService as any, + alertTypes, + defaultClusterState.clusterUuid, + start, + end, + log as any + ); + expect(status).toEqual({}); + }); + + it('should pass along the license service', async () => { + const customLicenseService = { + getWatcherFeature: jest.fn().mockImplementation(() => ({ + isAvailable: true, + isEnabled: true, + })), + }; + await fetchStatus( + alertsClient as any, + customLicenseService as any, + [ALERT_CLUSTER_HEALTH], + defaultClusterState.clusterUuid, + start, + end, + log as any + ); + expect(customLicenseService.getWatcherFeature).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts index 614658baf5c79..49e688fafbee5 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -4,56 +4,76 @@ * you may not use this file except in compliance with the Elastic License. */ import moment from 'moment'; -import { Logger } from '../../../../../../src/core/server'; -import { AlertCommonPerClusterState } from '../../alerts/types'; +import { AlertInstanceState } from '../../alerts/types'; import { AlertsClient } from '../../../../alerts/server'; +import { AlertsFactory } from '../../alerts'; +import { CommonAlertStatus, CommonAlertState, CommonAlertFilter } from '../../../common/types'; +import { ALERTS } from '../../../common/constants'; +import { MonitoringLicenseService } from '../../types'; export async function fetchStatus( alertsClient: AlertsClient, - alertTypes: string[], + licenseService: MonitoringLicenseService, + alertTypes: string[] | undefined, + clusterUuid: string, start: number, end: number, - log: Logger -): Promise { - const statuses = await Promise.all( - alertTypes.map( - (type) => - new Promise(async (resolve, reject) => { - // We need to get the id from the alertTypeId - const alerts = await alertsClient.find({ - options: { - filter: `alert.attributes.alertTypeId:${type}`, - }, - }); - if (alerts.total === 0) { - return resolve(false); - } + filters: CommonAlertFilter[] +): Promise<{ [type: string]: CommonAlertStatus }> { + const byType: { [type: string]: CommonAlertStatus } = {}; + await Promise.all( + (alertTypes || ALERTS).map(async (type) => { + const alert = await AlertsFactory.getByType(type, alertsClient); + if (!alert || !alert.isEnabled(licenseService)) { + return; + } + const serialized = alert.serialize(); + if (!serialized) { + return; + } - if (alerts.total !== 1) { - log.warn(`Found more than one alert for type ${type} which is unexpected.`); - } + const result: CommonAlertStatus = { + exists: false, + enabled: false, + states: [], + alert: serialized, + }; + + byType[type] = result; + + const id = alert.getId(); + if (!id) { + return result; + } + + result.exists = true; + result.enabled = true; - const id = alerts.data[0].id; + // Now that we have the id, we can get the state + const states = await alert.getStates(alertsClient, id, filters); + if (!states) { + return result; + } - // Now that we have the id, we can get the state - const states = await alertsClient.getAlertState({ id }); - if (!states || !states.alertTypeState) { - log.warn(`No alert states found for type ${type} which is unexpected.`); - return resolve(false); + result.states = Object.values(states).reduce((accum: CommonAlertState[], instance: any) => { + const alertInstanceState = instance.state as AlertInstanceState; + for (const state of alertInstanceState.alertStates) { + const meta = instance.meta; + if (clusterUuid && state.cluster.clusterUuid !== clusterUuid) { + return accum; } - const state = Object.values(states.alertTypeState)[0] as AlertCommonPerClusterState; + let firing = false; const isInBetween = moment(state.ui.resolvedMS).isBetween(start, end); if (state.ui.isFiring || isInBetween) { - return resolve({ - type, - ...state.ui, - }); + firing = true; } - return resolve(false); - }) - ) + accum.push({ firing, state, meta }); + } + return accum; + }, []); + }) ); - return statuses.filter(Boolean); + return byType; } diff --git a/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.test.ts deleted file mode 100644 index 1840a2026a753..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getPreparedAlert } from './get_prepared_alert'; -import { fetchClusters } from './fetch_clusters'; -import { fetchDefaultEmailAddress } from './fetch_default_email_address'; - -jest.mock('./fetch_clusters', () => ({ - fetchClusters: jest.fn(), -})); - -jest.mock('./fetch_default_email_address', () => ({ - fetchDefaultEmailAddress: jest.fn(), -})); - -describe('getPreparedAlert', () => { - const uiSettings = { get: jest.fn() }; - const alertType = 'test'; - const getUiSettingsService = async () => ({ - asScopedToClient: () => uiSettings, - }); - const monitoringCluster = null; - const logger = { warn: jest.fn() }; - const ccsEnabled = false; - const services = { - callCluster: jest.fn(), - savedObjectsClient: null, - }; - const emailAddress = 'foo@foo.com'; - const data = [{ foo: 1 }]; - const dataFetcher = () => data; - const clusterName = 'MonitoringCluster'; - const clusterUuid = 'sdf34sdf'; - const clusters = [{ clusterName, clusterUuid }]; - - afterEach(() => { - (uiSettings.get as jest.Mock).mockClear(); - (services.callCluster as jest.Mock).mockClear(); - (fetchClusters as jest.Mock).mockClear(); - (fetchDefaultEmailAddress as jest.Mock).mockClear(); - }); - - beforeEach(() => { - (fetchClusters as jest.Mock).mockImplementation(() => clusters); - (fetchDefaultEmailAddress as jest.Mock).mockImplementation(() => emailAddress); - }); - - it('should return fields as expected', async () => { - (uiSettings.get as jest.Mock).mockImplementation(() => { - return emailAddress; - }); - - const alert = await getPreparedAlert( - alertType, - getUiSettingsService as any, - monitoringCluster as any, - logger as any, - ccsEnabled, - services as any, - dataFetcher as any - ); - - expect(alert && alert.emailAddress).toBe(emailAddress); - expect(alert && alert.data).toBe(data); - }); - - it('should add ccs if specified', async () => { - const ccsClusterName = 'remoteCluster'; - (services.callCluster as jest.Mock).mockImplementation(() => { - return { - [ccsClusterName]: { - connected: true, - }, - }; - }); - - await getPreparedAlert( - alertType, - getUiSettingsService as any, - monitoringCluster as any, - logger as any, - true, - services as any, - dataFetcher as any - ); - - expect((fetchClusters as jest.Mock).mock.calls[0][1].includes(ccsClusterName)).toBe(true); - }); - - it('should ignore ccs if no remote clusters are available', async () => { - const ccsClusterName = 'remoteCluster'; - (services.callCluster as jest.Mock).mockImplementation(() => { - return { - [ccsClusterName]: { - connected: false, - }, - }; - }); - - await getPreparedAlert( - alertType, - getUiSettingsService as any, - monitoringCluster as any, - logger as any, - true, - services as any, - dataFetcher as any - ); - - expect((fetchClusters as jest.Mock).mock.calls[0][1].includes(ccsClusterName)).toBe(false); - }); - - it('should pass in the clusters into the data fetcher', async () => { - const customDataFetcher = jest.fn(() => data); - - await getPreparedAlert( - alertType, - getUiSettingsService as any, - monitoringCluster as any, - logger as any, - true, - services as any, - customDataFetcher as any - ); - - expect((customDataFetcher as jest.Mock).mock.calls[0][1]).toBe(clusters); - }); - - it('should return nothing if the data fetcher returns nothing', async () => { - const customDataFetcher = jest.fn(() => []); - - const result = await getPreparedAlert( - alertType, - getUiSettingsService as any, - monitoringCluster as any, - logger as any, - true, - services as any, - customDataFetcher as any - ); - - expect(result).toBe(null); - }); - - it('should return nothing if there is no email address', async () => { - (fetchDefaultEmailAddress as jest.Mock).mockImplementation(() => null); - - const result = await getPreparedAlert( - alertType, - getUiSettingsService as any, - monitoringCluster as any, - logger as any, - true, - services as any, - dataFetcher as any - ); - - expect(result).toBe(null); - }); -}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.ts b/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.ts deleted file mode 100644 index 1d307bc018a7b..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Logger, ILegacyCustomClusterClient, UiSettingsServiceStart } from 'kibana/server'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; -import { AlertServices } from '../../../../alerts/server'; -import { AlertCommonCluster } from '../../alerts/types'; -import { INDEX_PATTERN_ELASTICSEARCH } from '../../../common/constants'; -import { fetchAvailableCcs } from './fetch_available_ccs'; -import { getCcsIndexPattern } from './get_ccs_index_pattern'; -import { fetchClusters } from './fetch_clusters'; -import { fetchDefaultEmailAddress } from './fetch_default_email_address'; - -export interface PreparedAlert { - emailAddress: string; - clusters: AlertCommonCluster[]; - data: any[]; - timezone: string; - dateFormat: string; -} - -async function getCallCluster( - monitoringCluster: ILegacyCustomClusterClient, - services: Pick -): Promise { - if (!monitoringCluster) { - return services.callCluster; - } - - return monitoringCluster.callAsInternalUser; -} - -export async function getPreparedAlert( - alertType: string, - getUiSettingsService: () => Promise, - monitoringCluster: ILegacyCustomClusterClient, - logger: Logger, - ccsEnabled: boolean, - services: Pick, - dataFetcher: ( - callCluster: CallCluster, - clusters: AlertCommonCluster[], - esIndexPattern: string - ) => Promise -): Promise { - const callCluster = await getCallCluster(monitoringCluster, services); - - // Support CCS use cases by querying to find available remote clusters - // and then adding those to the index pattern we are searching against - let esIndexPattern = INDEX_PATTERN_ELASTICSEARCH; - if (ccsEnabled) { - const availableCcs = await fetchAvailableCcs(callCluster); - if (availableCcs.length > 0) { - esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); - } - } - - const clusters = await fetchClusters(callCluster, esIndexPattern); - - // Fetch the specific data - const data = await dataFetcher(callCluster, clusters, esIndexPattern); - if (data.length === 0) { - logger.warn(`No data found for ${alertType}.`); - return null; - } - - const uiSettings = (await getUiSettingsService()).asScopedToClient(services.savedObjectsClient); - const dateFormat: string = await uiSettings.get('dateFormat'); - const timezone: string = await uiSettings.get('dateFormat:tz'); - const emailAddress = await fetchDefaultEmailAddress(uiSettings); - if (!emailAddress) { - // TODO: we can do more here - logger.warn(`Unable to send email for ${alertType} because there is no email configured.`); - return null; - } - - return { - emailAddress, - data, - clusters, - dateFormat, - timezone, - }; -} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts deleted file mode 100644 index b99208bdde2c8..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import moment from 'moment-timezone'; -import { executeActions, getUiMessage } from './license_expiration.lib'; - -describe('licenseExpiration lib', () => { - describe('executeActions', () => { - const clusterName = 'clusterA'; - const instance: any = { scheduleActions: jest.fn() }; - const license: any = { clusterName }; - const $expiry = moment('2020-01-20'); - const dateFormat = 'dddd, MMMM Do YYYY, h:mm:ss a'; - const emailAddress = 'test@test.com'; - - beforeEach(() => { - instance.scheduleActions.mockClear(); - }); - - it('should schedule actions when firing', () => { - executeActions(instance, license, $expiry, dateFormat, emailAddress, false); - expect(instance.scheduleActions).toHaveBeenCalledWith('default', { - subject: 'NEW X-Pack Monitoring: License Expiration', - message: `Cluster '${clusterName}' license is going to expire on Monday, January 20th 2020, 12:00:00 am. Please update your license.`, - to: emailAddress, - }); - }); - - it('should schedule actions when resolved', () => { - executeActions(instance, license, $expiry, dateFormat, emailAddress, true); - expect(instance.scheduleActions).toHaveBeenCalledWith('default', { - subject: 'RESOLVED X-Pack Monitoring: License Expiration', - message: `This cluster alert has been resolved: Cluster '${clusterName}' license was going to expire on Monday, January 20th 2020, 12:00:00 am.`, - to: emailAddress, - }); - }); - }); - - describe('getUiMessage', () => { - it('should return a message when firing', () => { - const message = getUiMessage(false); - expect(message.text).toBe( - `This cluster's license is going to expire in #relative at #absolute. #start_linkPlease update your license.#end_link` - ); - // LOL How do I avoid this in TS???? - if (!message.tokens) { - return expect(false).toBe(true); - } - expect(message.tokens.length).toBe(3); - expect(message.tokens[0].startToken).toBe('#relative'); - expect(message.tokens[1].startToken).toBe('#absolute'); - expect(message.tokens[2].startToken).toBe('#start_link'); - expect(message.tokens[2].endToken).toBe('#end_link'); - }); - - it('should return a message when resolved', () => { - const message = getUiMessage(true); - expect(message.text).toBe(`This cluster's license is active.`); - expect(message.tokens).not.toBeDefined(); - }); - }); -}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts b/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts deleted file mode 100644 index 97ef2790b516d..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { Moment } from 'moment-timezone'; -import { i18n } from '@kbn/i18n'; -import { AlertInstance } from '../../../../alerts/server'; -import { - AlertCommonPerClusterMessageLinkToken, - AlertCommonPerClusterMessageTimeToken, - AlertCommonCluster, - AlertCommonPerClusterMessage, -} from '../../alerts/types'; -import { AlertCommonPerClusterMessageTokenType } from '../../alerts/enums'; - -const RESOLVED_SUBJECT = i18n.translate( - 'xpack.monitoring.alerts.licenseExpiration.resolvedSubject', - { - defaultMessage: 'RESOLVED X-Pack Monitoring: License Expiration', - } -); - -const NEW_SUBJECT = i18n.translate('xpack.monitoring.alerts.licenseExpiration.newSubject', { - defaultMessage: 'NEW X-Pack Monitoring: License Expiration', -}); - -export function executeActions( - instance: AlertInstance, - cluster: AlertCommonCluster, - $expiry: Moment, - dateFormat: string, - emailAddress: string, - resolved: boolean = false -) { - if (resolved) { - instance.scheduleActions('default', { - subject: RESOLVED_SUBJECT, - message: `This cluster alert has been resolved: Cluster '${ - cluster.clusterName - }' license was going to expire on ${$expiry.format(dateFormat)}.`, - to: emailAddress, - }); - } else { - instance.scheduleActions('default', { - subject: NEW_SUBJECT, - message: `Cluster '${cluster.clusterName}' license is going to expire on ${$expiry.format( - dateFormat - )}. Please update your license.`, - to: emailAddress, - }); - } -} - -export function getUiMessage(resolved: boolean = false): AlertCommonPerClusterMessage { - if (resolved) { - return { - text: i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.resolvedMessage', { - defaultMessage: `This cluster's license is active.`, - }), - }; - } - return { - text: i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.firingMessage', { - defaultMessage: `This cluster's license is going to expire in #relative at #absolute. #start_linkPlease update your license.#end_link`, - }), - tokens: [ - { - startToken: '#relative', - type: AlertCommonPerClusterMessageTokenType.Time, - isRelative: true, - isAbsolute: false, - } as AlertCommonPerClusterMessageTimeToken, - { - startToken: '#absolute', - type: AlertCommonPerClusterMessageTokenType.Time, - isAbsolute: true, - isRelative: false, - } as AlertCommonPerClusterMessageTimeToken, - { - startToken: '#start_link', - endToken: '#end_link', - type: AlertCommonPerClusterMessageTokenType.Link, - url: 'license', - } as AlertCommonPerClusterMessageLinkToken, - ], - }; -} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/map_legacy_severity.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/map_legacy_severity.test.ts new file mode 100644 index 0000000000000..11a1c6eb1a6d6 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/map_legacy_severity.test.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AlertSeverity } from '../../../common/enums'; +import { mapLegacySeverity } from './map_legacy_severity'; + +describe('mapLegacySeverity', () => { + it('should map it', () => { + expect(mapLegacySeverity(500)).toBe(AlertSeverity.Warning); + expect(mapLegacySeverity(1000)).toBe(AlertSeverity.Warning); + expect(mapLegacySeverity(2000)).toBe(AlertSeverity.Danger); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/map_legacy_severity.ts b/x-pack/plugins/monitoring/server/lib/alerts/map_legacy_severity.ts new file mode 100644 index 0000000000000..5687c0c15b03b --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/map_legacy_severity.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AlertSeverity } from '../../../common/enums'; + +export function mapLegacySeverity(severity: number) { + const floor = Math.floor(severity / 1000); + if (floor <= 1) { + return AlertSeverity.Warning; + } + return AlertSeverity.Danger; +} diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js index 5ed8d6b01aba5..50a4df8a3ff57 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js @@ -13,13 +13,10 @@ import { getKibanasForClusters } from '../kibana'; import { getLogstashForClusters } from '../logstash'; import { getLogstashPipelineIds } from '../logstash/get_pipeline_ids'; import { getBeatsForClusters } from '../beats'; -import { alertsClustersAggregation } from '../../cluster_alerts/alerts_clusters_aggregation'; -import { alertsClusterSearch } from '../../cluster_alerts/alerts_cluster_search'; +import { verifyMonitoringLicense } from '../../cluster_alerts/verify_monitoring_license'; import { checkLicense as checkLicenseForAlerts } from '../../cluster_alerts/check_license'; -import { fetchStatus } from '../alerts/fetch_status'; import { getClustersSummary } from './get_clusters_summary'; import { - CLUSTER_ALERTS_SEARCH_SIZE, STANDALONE_CLUSTER_CLUSTER_UUID, CODE_PATH_ML, CODE_PATH_ALERTS, @@ -28,12 +25,11 @@ import { CODE_PATH_LOGSTASH, CODE_PATH_BEATS, CODE_PATH_APM, - KIBANA_ALERTING_ENABLED, - ALERT_TYPES, } from '../../../common/constants'; import { getApmsForClusters } from '../apm/get_apms_for_clusters'; import { i18n } from '@kbn/i18n'; import { checkCcrEnabled } from '../elasticsearch/ccr'; +import { fetchStatus } from '../alerts/fetch_status'; import { getStandaloneClusterDefinition, hasStandaloneClusters } from '../standalone_clusters'; import { getLogTypes } from '../logs'; import { isInCodePath } from './is_in_code_path'; @@ -52,7 +48,6 @@ export async function getClustersFromRequest( lsIndexPattern, beatsIndexPattern, apmIndexPattern, - alertsIndex, filebeatIndexPattern, } = indexPatterns; @@ -101,25 +96,6 @@ export async function getClustersFromRequest( cluster.ml = { jobs: mlJobs }; } - if (isInCodePath(codePaths, [CODE_PATH_ALERTS])) { - if (KIBANA_ALERTING_ENABLED) { - const alertsClient = req.getAlertsClient ? req.getAlertsClient() : null; - cluster.alerts = await fetchStatus(alertsClient, ALERT_TYPES, start, end, req.logger); - } else { - cluster.alerts = await alertsClusterSearch( - req, - alertsIndex, - cluster, - checkLicenseForAlerts, - { - start, - end, - size: CLUSTER_ALERTS_SEARCH_SIZE, - } - ); - } - } - cluster.logs = isInCodePath(codePaths, [CODE_PATH_LOGS]) ? await getLogTypes(req, filebeatIndexPattern, { clusterUuid: cluster.cluster_uuid, @@ -141,21 +117,67 @@ export async function getClustersFromRequest( // add alerts data if (isInCodePath(codePaths, [CODE_PATH_ALERTS])) { - const clustersAlerts = await alertsClustersAggregation( - req, - alertsIndex, - clusters, - checkLicenseForAlerts - ); - clusters.forEach((cluster) => { + const alertsClient = req.getAlertsClient(); + for (const cluster of clusters) { + const verification = verifyMonitoringLicense(req.server); + if (!verification.enabled) { + // return metadata detailing that alerts is disabled because of the monitoring cluster license + cluster.alerts = { + alertsMeta: { + enabled: verification.enabled, + message: verification.message, // NOTE: this is only defined when the alert feature is disabled + }, + list: {}, + }; + continue; + } + + // check the license type of the production cluster for alerts feature support + const license = cluster.license || {}; + const prodLicenseInfo = checkLicenseForAlerts( + license.type, + license.status === 'active', + 'production' + ); + if (prodLicenseInfo.clusterAlerts.enabled) { + cluster.alerts = { + list: await fetchStatus( + alertsClient, + req.server.plugins.monitoring.info, + undefined, + cluster.cluster_uuid, + start, + end, + [] + ), + alertsMeta: { + enabled: true, + }, + }; + continue; + } + cluster.alerts = { + list: {}, alertsMeta: { - enabled: clustersAlerts.alertsMeta.enabled, - message: clustersAlerts.alertsMeta.message, // NOTE: this is only defined when the alert feature is disabled + enabled: true, + }, + clusterMeta: { + enabled: false, + message: i18n.translate( + 'xpack.monitoring.clusterAlerts.unsupportedClusterAlertsDescription', + { + defaultMessage: + 'Cluster [{clusterName}] license type [{licenseType}] does not support Cluster Alerts', + values: { + clusterName: cluster.cluster_name, + licenseType: `${license.type}`, + }, + } + ), }, - ...clustersAlerts[cluster.cluster_uuid], }; - }); + } } } diff --git a/x-pack/plugins/monitoring/server/lib/errors/handle_error.js b/x-pack/plugins/monitoring/server/lib/errors/handle_error.js index d6549a8fa98e9..4726020210ce7 100644 --- a/x-pack/plugins/monitoring/server/lib/errors/handle_error.js +++ b/x-pack/plugins/monitoring/server/lib/errors/handle_error.js @@ -9,7 +9,7 @@ import { isKnownError, handleKnownError } from './known_errors'; import { isAuthError, handleAuthError } from './auth_errors'; export function handleError(err, req) { - req.logger.error(err); + req && req.logger && req.logger.error(err); // specially handle auth errors if (isAuthError(err)) { diff --git a/x-pack/plugins/monitoring/server/license_service.ts b/x-pack/plugins/monitoring/server/license_service.ts index 7dcdf8897f6a1..fb45abc22afa4 100644 --- a/x-pack/plugins/monitoring/server/license_service.ts +++ b/x-pack/plugins/monitoring/server/license_service.ts @@ -46,7 +46,7 @@ export class LicenseService { license$, getMessage: () => rawLicense?.getUnavailableReason() || 'N/A', getMonitoringFeature: () => rawLicense?.getFeature('monitoring') || defaultLicenseFeature, - getWatcherFeature: () => rawLicense?.getFeature('monitoring') || defaultLicenseFeature, + getWatcherFeature: () => rawLicense?.getFeature('watcher') || defaultLicenseFeature, getSecurityFeature: () => rawLicense?.getFeature('security') || defaultLicenseFeature, stop: () => { if (licenseSubscription) { diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 7c346e007da23..5f358badde401 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -9,8 +9,6 @@ import { first, map } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { has, get } from 'lodash'; import { TypeOf } from '@kbn/config-schema'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; import { Logger, PluginInitializerContext, @@ -20,15 +18,12 @@ import { CoreSetup, ILegacyCustomClusterClient, CoreStart, - IRouter, - ILegacyClusterClient, CustomHttpResponseOptions, ResponseError, } from 'kibana/server'; import { LOGGING_TAG, KIBANA_MONITORING_LOGGING_TAG, - KIBANA_ALERTING_ENABLED, KIBANA_STATS_TYPE_MONITORING, } from '../common/constants'; import { MonitoringConfig, createConfig, configSchema } from './config'; @@ -41,56 +36,18 @@ import { initInfraSource } from './lib/logs/init_infra_source'; import { instantiateClient } from './es_client/instantiate_client'; import { registerCollectors } from './kibana_monitoring/collectors'; import { registerMonitoringCollection } from './telemetry_collection'; -import { LicensingPluginSetup } from '../../licensing/server'; -import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; import { LicenseService } from './license_service'; -import { MonitoringLicenseService } from './types'; +import { AlertsFactory } from './alerts'; import { - PluginStartContract as AlertingPluginStartContract, - PluginSetupContract as AlertingPluginSetupContract, -} from '../../alerts/server'; -import { getLicenseExpiration } from './alerts/license_expiration'; -import { getClusterState } from './alerts/cluster_state'; -import { InfraPluginSetup } from '../../infra/server'; - -export interface LegacyAPI { - getServerStatus: () => string; -} - -interface PluginsSetup { - telemetryCollectionManager?: TelemetryCollectionManagerPluginSetup; - usageCollection?: UsageCollectionSetup; - licensing: LicensingPluginSetup; - features: FeaturesPluginSetupContract; - alerts: AlertingPluginSetupContract; - infra: InfraPluginSetup; -} - -interface PluginsStart { - alerts: AlertingPluginStartContract; -} - -interface MonitoringCoreConfig { - get: (key: string) => string | undefined; -} - -interface MonitoringCore { - config: () => MonitoringCoreConfig; - log: Logger; - route: (options: any) => void; -} - -interface LegacyShimDependencies { - router: IRouter; - instanceUuid: string; - esDataClient: ILegacyClusterClient; - kibanaStatsCollector: any; -} - -interface IBulkUploader { - setKibanaStatusGetter: (getter: () => string | undefined) => void; - getKibanaStats: () => any; -} + MonitoringCore, + MonitoringLicenseService, + LegacyShimDependencies, + IBulkUploader, + PluginsSetup, + PluginsStart, + LegacyAPI, + LegacyRequest, +} from './types'; // This is used to test the version of kibana const snapshotRegex = /-snapshot/i; @@ -131,8 +88,9 @@ export class Plugin { .pipe(first()) .toPromise(); + const router = core.http.createRouter(); this.legacyShimDependencies = { - router: core.http.createRouter(), + router, instanceUuid: core.uuid.getInstanceUuid(), esDataClient: core.elasticsearch.legacy.client, kibanaStatsCollector: plugins.usageCollection?.getCollectorByType( @@ -158,29 +116,20 @@ export class Plugin { }); await this.licenseService.refresh(); - if (KIBANA_ALERTING_ENABLED) { - plugins.alerts.registerType( - getLicenseExpiration( - async () => { - const coreStart = (await core.getStartServices())[0]; - return coreStart.uiSettings; - }, - cluster, - this.getLogger, - config.ui.ccs.enabled - ) - ); - plugins.alerts.registerType( - getClusterState( - async () => { - const coreStart = (await core.getStartServices())[0]; - return coreStart.uiSettings; - }, - cluster, - this.getLogger, - config.ui.ccs.enabled - ) - ); + const serverInfo = core.http.getServerInfo(); + let kibanaUrl = `${serverInfo.protocol}://${serverInfo.hostname}:${serverInfo.port}`; + if (core.http.basePath.serverBasePath) { + kibanaUrl += `/${core.http.basePath.serverBasePath}`; + } + const getUiSettingsService = async () => { + const coreStart = (await core.getStartServices())[0]; + return coreStart.uiSettings; + }; + + const alerts = AlertsFactory.getAll(); + for (const alert of alerts) { + alert.initializeAlertType(getUiSettingsService, cluster, this.getLogger, config, kibanaUrl); + plugins.alerts.registerType(alert.getAlertType()); } // Initialize telemetry @@ -200,7 +149,6 @@ export class Plugin { const kibanaCollectionEnabled = config.kibana.collection.enabled; if (kibanaCollectionEnabled) { // Start kibana internal collection - const serverInfo = core.http.getServerInfo(); const bulkUploader = (this.bulkUploader = initBulkUploader({ elasticsearch: core.elasticsearch, config, @@ -252,7 +200,10 @@ export class Plugin { ); this.registerPluginInUI(plugins); - requireUIRoutes(this.monitoringCore); + requireUIRoutes(this.monitoringCore, { + router, + licenseService: this.licenseService, + }); initInfraSource(config, plugins.infra); } @@ -353,14 +304,16 @@ export class Plugin { res: KibanaResponseFactory ) => { const plugins = (await getCoreServices())[1]; - const legacyRequest = { + const legacyRequest: LegacyRequest = { ...req, logger: this.log, getLogger: this.getLogger, payload: req.body, getKibanaStatsCollector: () => this.legacyShimDependencies.kibanaStatsCollector, getUiSettingsService: () => context.core.uiSettings.client, + getActionTypeRegistry: () => context.actions?.listTypes(), getAlertsClient: () => plugins.alerts.getAlertsClientWithRequest(req), + getActionsClient: () => plugins.actions.getActionsClientWithRequest(req), server: { config: legacyConfigWrapper, newPlatform: { @@ -388,7 +341,8 @@ export class Plugin { const result = await options.handler(legacyRequest); return res.ok({ body: result }); } catch (err) { - const statusCode: number = err.output?.statusCode || err.statusCode || err.status; + const statusCode: number = + err.output?.statusCode || err.statusCode || err.status || 500; if (Boom.isBoom(err) || statusCode !== 500) { return res.customError({ statusCode, body: err }); } diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/alerts.js b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/alerts.js deleted file mode 100644 index d5a43d32f600a..0000000000000 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/alerts.js +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { schema } from '@kbn/config-schema'; -import { isFunction } from 'lodash'; -import { - ALERT_TYPE_LICENSE_EXPIRATION, - ALERT_TYPE_CLUSTER_STATE, - MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, - ALERT_TYPES, -} from '../../../../../common/constants'; -import { handleError } from '../../../../lib/errors'; -import { fetchStatus } from '../../../../lib/alerts/fetch_status'; - -async function createAlerts(req, alertsClient, { selectedEmailActionId }) { - const createdAlerts = []; - - // Create alerts - const ALERT_TYPES = { - [ALERT_TYPE_LICENSE_EXPIRATION]: { - schedule: { interval: '1m' }, - actions: [ - { - group: 'default', - id: selectedEmailActionId, - params: { - subject: '{{context.subject}}', - message: `{{context.message}}`, - to: ['{{context.to}}'], - }, - }, - ], - }, - [ALERT_TYPE_CLUSTER_STATE]: { - schedule: { interval: '1m' }, - actions: [ - { - group: 'default', - id: selectedEmailActionId, - params: { - subject: '{{context.subject}}', - message: `{{context.message}}`, - to: ['{{context.to}}'], - }, - }, - ], - }, - }; - - for (const alertTypeId of Object.keys(ALERT_TYPES)) { - const existingAlert = await alertsClient.find({ - options: { - search: alertTypeId, - }, - }); - if (existingAlert.total === 1) { - await alertsClient.delete({ id: existingAlert.data[0].id }); - } - - const result = await alertsClient.create({ - data: { - enabled: true, - alertTypeId, - ...ALERT_TYPES[alertTypeId], - }, - }); - createdAlerts.push(result); - } - - return createdAlerts; -} - -async function saveEmailAddress(emailAddress, uiSettingsService) { - await uiSettingsService.set(MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, emailAddress); -} - -export function createKibanaAlertsRoute(server) { - server.route({ - method: 'POST', - path: '/api/monitoring/v1/alerts', - config: { - validate: { - payload: schema.object({ - selectedEmailActionId: schema.string(), - emailAddress: schema.string(), - }), - }, - }, - async handler(req, headers) { - const { emailAddress, selectedEmailActionId } = req.payload; - const alertsClient = isFunction(req.getAlertsClient) ? req.getAlertsClient() : null; - if (!alertsClient) { - return headers.response().code(404); - } - - const [alerts, emailResponse] = await Promise.all([ - createAlerts(req, alertsClient, { ...req.params, selectedEmailActionId }), - saveEmailAddress(emailAddress, req.getUiSettingsService()), - ]); - - return { alerts, emailResponse }; - }, - }); - - server.route({ - method: 'POST', - path: '/api/monitoring/v1/alert_status', - config: { - validate: { - payload: schema.object({ - timeRange: schema.object({ - min: schema.string(), - max: schema.string(), - }), - }), - }, - }, - async handler(req, headers) { - const alertsClient = isFunction(req.getAlertsClient) ? req.getAlertsClient() : null; - if (!alertsClient) { - return headers.response().code(404); - } - - const start = req.payload.timeRange.min; - const end = req.payload.timeRange.max; - let alerts; - - try { - alerts = await fetchStatus(alertsClient, ALERT_TYPES, start, end, req.logger); - } catch (err) { - throw handleError(err, req); - } - - return { alerts }; - }, - }); -} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts new file mode 100644 index 0000000000000..1d83644fce756 --- /dev/null +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// @ts-ignore +import { handleError } from '../../../../lib/errors'; +import { AlertsFactory } from '../../../../alerts'; +import { RouteDependencies } from '../../../../types'; +import { ALERT_ACTION_TYPE_LOG } from '../../../../../common/constants'; +import { ActionResult } from '../../../../../../actions/common'; +// import { fetchDefaultEmailAddress } from '../../../../lib/alerts/fetch_default_email_address'; + +const DEFAULT_SERVER_LOG_NAME = 'Monitoring: Write to Kibana log'; + +export function enableAlertsRoute(server: any, npRoute: RouteDependencies) { + npRoute.router.post( + { + path: '/api/monitoring/v1/alerts/enable', + options: { tags: ['access:monitoring'] }, + validate: false, + }, + async (context, request, response) => { + try { + const alertsClient = context.alerting?.getAlertsClient(); + const actionsClient = context.actions?.getActionsClient(); + const types = context.actions?.listTypes(); + if (!alertsClient || !actionsClient || !types) { + return response.notFound(); + } + + // Get or create the default log action + let serverLogAction; + const allActions = await actionsClient.getAll(); + for (const action of allActions) { + if (action.name === DEFAULT_SERVER_LOG_NAME) { + serverLogAction = action as ActionResult; + break; + } + } + + if (!serverLogAction) { + serverLogAction = await actionsClient.create({ + action: { + name: DEFAULT_SERVER_LOG_NAME, + actionTypeId: ALERT_ACTION_TYPE_LOG, + config: {}, + secrets: {}, + }, + }); + } + + const actions = [ + { + id: serverLogAction.id, + config: {}, + }, + ]; + + const alerts = AlertsFactory.getAll().filter((a) => a.isEnabled(npRoute.licenseService)); + const createdAlerts = await Promise.all( + alerts.map( + async (alert) => await alert.createIfDoesNotExist(alertsClient, actionsClient, actions) + ) + ); + return response.ok({ body: createdAlerts }); + } catch (err) { + throw handleError(err); + } + } + ); +} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/index.js b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/index.js index 246cdfde97cff..a41562dd29a88 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/index.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/index.js @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './legacy_alerts'; -export * from './alerts'; +export { enableAlertsRoute } from './enable'; +export { alertStatusRoute } from './status'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/legacy_alerts.js b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/legacy_alerts.js deleted file mode 100644 index 688caac9b60b1..0000000000000 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/legacy_alerts.js +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { schema } from '@kbn/config-schema'; -import { alertsClusterSearch } from '../../../../cluster_alerts/alerts_cluster_search'; -import { checkLicense } from '../../../../cluster_alerts/check_license'; -import { getClusterLicense } from '../../../../lib/cluster/get_cluster_license'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; -import { INDEX_PATTERN_ELASTICSEARCH, INDEX_ALERTS } from '../../../../../common/constants'; - -/* - * Cluster Alerts route. - */ -export function legacyClusterAlertsRoute(server) { - server.route({ - method: 'POST', - path: '/api/monitoring/v1/clusters/{clusterUuid}/legacy_alerts', - config: { - validate: { - params: schema.object({ - clusterUuid: schema.string(), - }), - payload: schema.object({ - ccs: schema.maybe(schema.string()), - timeRange: schema.object({ - min: schema.string(), - max: schema.string(), - }), - }), - }, - }, - handler(req) { - const config = server.config(); - const ccs = req.payload.ccs; - const clusterUuid = req.params.clusterUuid; - const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs); - const alertsIndex = prefixIndexPattern(config, INDEX_ALERTS, ccs); - const options = { - start: req.payload.timeRange.min, - end: req.payload.timeRange.max, - }; - - return getClusterLicense(req, esIndexPattern, clusterUuid).then((license) => - alertsClusterSearch( - req, - alertsIndex, - { cluster_uuid: clusterUuid, license }, - checkLicense, - options - ) - ); - }, - }); -} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts new file mode 100644 index 0000000000000..eef99bbc4ac68 --- /dev/null +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +// @ts-ignore +import { handleError } from '../../../../lib/errors'; +import { RouteDependencies } from '../../../../types'; +import { fetchStatus } from '../../../../lib/alerts/fetch_status'; +import { CommonAlertFilter } from '../../../../../common/types'; + +export function alertStatusRoute(server: any, npRoute: RouteDependencies) { + npRoute.router.post( + { + path: '/api/monitoring/v1/alert/{clusterUuid}/status', + options: { tags: ['access:monitoring'] }, + validate: { + params: schema.object({ + clusterUuid: schema.string(), + }), + body: schema.object({ + alertTypeIds: schema.maybe(schema.arrayOf(schema.string())), + filters: schema.maybe(schema.arrayOf(schema.any())), + timeRange: schema.object({ + min: schema.number(), + max: schema.number(), + }), + }), + }, + }, + async (context, request, response) => { + try { + const { clusterUuid } = request.params; + const { + alertTypeIds, + timeRange: { min, max }, + filters, + } = request.body; + const alertsClient = context.alerting?.getAlertsClient(); + if (!alertsClient) { + return response.notFound(); + } + + const status = await fetchStatus( + alertsClient, + npRoute.licenseService, + alertTypeIds, + clusterUuid, + min, + max, + filters as CommonAlertFilter[] + ); + return response.ok({ body: status }); + } catch (err) { + throw handleError(err); + } + } + ); +} diff --git a/x-pack/plugins/monitoring/server/routes/index.js b/x-pack/plugins/monitoring/server/routes/index.ts similarity index 67% rename from x-pack/plugins/monitoring/server/routes/index.js rename to x-pack/plugins/monitoring/server/routes/index.ts index 0aefed4d9a507..69ded6ad5a5f0 100644 --- a/x-pack/plugins/monitoring/server/routes/index.js +++ b/x-pack/plugins/monitoring/server/routes/index.ts @@ -4,14 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -/*eslint import/namespace: ['error', { allowComputed: true }]*/ +/* eslint import/namespace: ['error', { allowComputed: true }]*/ +// @ts-ignore import * as uiRoutes from './api/v1/ui'; // namespace import +import { RouteDependencies } from '../types'; -export function requireUIRoutes(server) { +export function requireUIRoutes(server: any, npRoute: RouteDependencies) { const routes = Object.keys(uiRoutes); routes.forEach((route) => { const registerRoute = uiRoutes[route]; // computed reference to module objects imported via namespace - registerRoute(server); + registerRoute(server, npRoute); }); } diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index 9b3725d007fd9..0c346c8082475 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -4,7 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ import { Observable } from 'rxjs'; +import { IRouter, ILegacyClusterClient, Logger } from 'kibana/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; import { LicenseFeature, ILicense } from '../../licensing/server'; +import { PluginStartContract as ActionsPluginsStartContact } from '../../actions/server'; +import { + PluginStartContract as AlertingPluginStartContract, + PluginSetupContract as AlertingPluginSetupContract, +} from '../../alerts/server'; +import { InfraPluginSetup } from '../../infra/server'; +import { LicensingPluginSetup } from '../../licensing/server'; +import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; export interface MonitoringLicenseService { refresh: () => Promise; @@ -15,3 +26,85 @@ export interface MonitoringLicenseService { getSecurityFeature: () => LicenseFeature; stop: () => void; } + +export interface MonitoringElasticsearchConfig { + hosts: string[]; +} + +export interface LegacyAPI { + getServerStatus: () => string; +} + +export interface PluginsSetup { + telemetryCollectionManager?: TelemetryCollectionManagerPluginSetup; + usageCollection?: UsageCollectionSetup; + licensing: LicensingPluginSetup; + features: FeaturesPluginSetupContract; + alerts: AlertingPluginSetupContract; + infra: InfraPluginSetup; +} + +export interface PluginsStart { + alerts: AlertingPluginStartContract; + actions: ActionsPluginsStartContact; +} + +export interface MonitoringCoreConfig { + get: (key: string) => string | undefined; +} + +export interface RouteDependencies { + router: IRouter; + licenseService: MonitoringLicenseService; +} + +export interface MonitoringCore { + config: () => MonitoringCoreConfig; + log: Logger; + route: (options: any) => void; +} + +export interface LegacyShimDependencies { + router: IRouter; + instanceUuid: string; + esDataClient: ILegacyClusterClient; + kibanaStatsCollector: any; +} + +export interface IBulkUploader { + setKibanaStatusGetter: (getter: () => string | undefined) => void; + getKibanaStats: () => any; +} + +export interface LegacyRequest { + logger: Logger; + getLogger: (...scopes: string[]) => Logger; + payload: unknown; + getKibanaStatsCollector: () => any; + getUiSettingsService: () => any; + getActionTypeRegistry: () => any; + getAlertsClient: () => any; + getActionsClient: () => any; + server: { + config: () => { + get: (key: string) => string | undefined; + }; + newPlatform: { + setup: { + plugins: PluginsStart; + }; + }; + plugins: { + monitoring: { + info: MonitoringLicenseService; + }; + elasticsearch: { + getCluster: ( + name: string + ) => { + callWithRequest: (req: any, endpoint: string, params: any) => Promise; + }; + }; + }; + }; +} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2a8365a8bc5c9..6ef8a61f93295 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10902,86 +10902,9 @@ "xpack.monitoring.ajaxErrorHandler.requestErrorNotificationTitle": "監視リクエストエラー", "xpack.monitoring.ajaxErrorHandler.requestFailedNotification.retryButtonLabel": "再試行", "xpack.monitoring.ajaxErrorHandler.requestFailedNotificationTitle": "監視リクエスト失敗", - "xpack.monitoring.alertingEmailAddress.description": "スタック監視からアラートを受信するデフォルトメールアドレス", - "xpack.monitoring.alertingEmailAddress.name": "アラートメールアドレス", - "xpack.monitoring.alerts.categoryColumn.generalLabel": "一般", - "xpack.monitoring.alerts.categoryColumnTitle": "カテゴリー", - "xpack.monitoring.alerts.clusterAlertsTitle": "クラスターアラート", - "xpack.monitoring.alerts.clusterOverviewLinkLabel": "« クラスターの概要", - "xpack.monitoring.alerts.clusterState.actionGroups.default": "デフォルト", - "xpack.monitoring.alerts.clusterStatus.newSubject": "NEW X-Pack監視:クラスターステータス", - "xpack.monitoring.alerts.clusterStatus.redMessage": "見つからないプライマリおよびレプリカシャードを割り当て", - "xpack.monitoring.alerts.clusterStatus.resolvedSubject": "RESOLVED X-Pack監視:クラスターステータス", - "xpack.monitoring.alerts.clusterStatus.ui.firingMessage": "Elasticsearchクラスターステータスは{status}です。 #start_link{message}#end_link", - "xpack.monitoring.alerts.clusterStatus.ui.resolvedMessage": "Elasticsearchクラスターステータスは緑です。", - "xpack.monitoring.alerts.clusterStatus.yellowMessage": "見つからないレプリカシャードを割り当て", - "xpack.monitoring.alerts.configuration.confirm": "確認して保存", - "xpack.monitoring.alerts.configuration.createEmailAction": "メールアクションを作成", - "xpack.monitoring.alerts.configuration.deleteConfiguration.buttonText": "削除", - "xpack.monitoring.alerts.configuration.editConfiguration.buttonText": "編集", - "xpack.monitoring.alerts.configuration.emailAction.name": "スタック監視アラートのメールアクション", - "xpack.monitoring.alerts.configuration.emailAddressLabel": "メールアドレス", - "xpack.monitoring.alerts.configuration.newActionDropdownDisplay": "新しいメールアクションを作成...", - "xpack.monitoring.alerts.configuration.save": "保存", - "xpack.monitoring.alerts.configuration.securityConfigurationError.docsLinkLabel": "ドキュメント", - "xpack.monitoring.alerts.configuration.securityConfigurationErrorMessage": "{link} を参照して API キーを有効にします。", - "xpack.monitoring.alerts.configuration.securityConfigurationErrorTitle": "Elasticsearch で API キーが有効になっていません", - "xpack.monitoring.alerts.configuration.selectAction.inputDisplay": "送信元: {from}、サービス: {service}", - "xpack.monitoring.alerts.configuration.selectEmailAction": "メールアクションを選択", - "xpack.monitoring.alerts.configuration.setEmailAddress": "アラートを受信するようにメールを設定します", - "xpack.monitoring.alerts.configuration.step1.editAction": "以下のアクションを編集してください。", - "xpack.monitoring.alerts.configuration.step1.testingError": "テストメールを送信できません。電子メール構成を再確認してください。", - "xpack.monitoring.alerts.configuration.step3.saveError": "を保存できませんでした", - "xpack.monitoring.alerts.configuration.testConfiguration.buttonText": "テスト", - "xpack.monitoring.alerts.configuration.testConfiguration.disabledTooltipText": "以下のメールアドレスを構成してこのアクションをテストします。", - "xpack.monitoring.alerts.configuration.testConfiguration.success": "こちら側からは良好に見えます。", - "xpack.monitoring.alerts.configuration.unknownError": "何か問題が発生しましたサーバーログを参照してください。", - "xpack.monitoring.alerts.filterAlertsPlaceholder": "フィルターアラート…", - "xpack.monitoring.alerts.highSeverityName": "高", - "xpack.monitoring.alerts.lastCheckedColumnTitle": "最終確認", - "xpack.monitoring.alerts.licenseExpiration.actionGroups.default": "デフォルト", - "xpack.monitoring.alerts.licenseExpiration.newSubject": "NEW X-Pack 監視:ライセンス期限", - "xpack.monitoring.alerts.licenseExpiration.resolvedSubject": "RESOLVED X-Pack 監視:ライセンス期限", "xpack.monitoring.alerts.licenseExpiration.ui.firingMessage": "このクラスターのライセンスは#absoluteの#relativeに期限切れになります。#start_linkライセンスを更新してください。#end_link", "xpack.monitoring.alerts.licenseExpiration.ui.resolvedMessage": "このクラスターのライセンスはアクティブです。", - "xpack.monitoring.alerts.lowSeverityName": "低", - "xpack.monitoring.alerts.mediumSeverityName": "中", - "xpack.monitoring.alerts.messageColumnTitle": "メッセージ", - "xpack.monitoring.alerts.migrate.manageAction.addingNewServiceText": "新しいサービスを追加中...", - "xpack.monitoring.alerts.migrate.manageAction.addNewServiceText": "新しいサービスを追加...", - "xpack.monitoring.alerts.migrate.manageAction.cancelLabel": "キャンセル", - "xpack.monitoring.alerts.migrate.manageAction.createLabel": "メールアクションを作成", - "xpack.monitoring.alerts.migrate.manageAction.fromHelpText": "アラートの送信元メールアドレス", - "xpack.monitoring.alerts.migrate.manageAction.fromText": "開始:", - "xpack.monitoring.alerts.migrate.manageAction.hostHelpText": "サービスプロバイダーのホスト名", - "xpack.monitoring.alerts.migrate.manageAction.hostText": "ホスト", - "xpack.monitoring.alerts.migrate.manageAction.passwordHelpText": "サービスプロバイダーとともに使用するパスワード", - "xpack.monitoring.alerts.migrate.manageAction.passwordText": "パスワード", - "xpack.monitoring.alerts.migrate.manageAction.portHelpText": "サービスプロバイダーのポート番号", - "xpack.monitoring.alerts.migrate.manageAction.portText": "ポート", "xpack.monitoring.alerts.migrate.manageAction.requiredFieldError": "{field} は必須フィールドです。", - "xpack.monitoring.alerts.migrate.manageAction.saveLabel": "メールアクションを保存", - "xpack.monitoring.alerts.migrate.manageAction.secureHelpText": "サービスプロバイダーと TLS を使用するかどうか", - "xpack.monitoring.alerts.migrate.manageAction.secureText": "セキュア", - "xpack.monitoring.alerts.migrate.manageAction.serviceHelpText": "詳細情報", - "xpack.monitoring.alerts.migrate.manageAction.serviceText": "サービス", - "xpack.monitoring.alerts.migrate.manageAction.userHelpText": "サービスプロバイダーとともに使用するユーザー", - "xpack.monitoring.alerts.migrate.manageAction.userText": "ユーザー", - "xpack.monitoring.alerts.notResolvedDescription": "未解決", - "xpack.monitoring.alerts.resolvedAgoDescription": "{duration} 前", - "xpack.monitoring.alerts.resolvedColumnTitle": "解決済み", - "xpack.monitoring.alerts.severityTitle": "{severity}深刻度アラート", - "xpack.monitoring.alerts.severityTitle.unknown": "不明", - "xpack.monitoring.alerts.severityValue.unknown": "N/A", - "xpack.monitoring.alerts.status.flyoutSubtitle": "アラートを受信するようにメールサーバーとメールアドレスを構成します。", - "xpack.monitoring.alerts.status.flyoutTitle": "監視アラート", - "xpack.monitoring.alerts.status.manage": "変更を加えますか?ここをクリック。", - "xpack.monitoring.alerts.status.needToMigrate": "クラスターアラートを新しいアラートプラットフォームに移行します。", - "xpack.monitoring.alerts.status.needToMigrateTitle": "こんにちは、アラートの改善を図りました。", - "xpack.monitoring.alerts.status.upToDate": "Kibana アラートは最新です。", - "xpack.monitoring.alerts.statusColumnTitle": "ステータス", - "xpack.monitoring.alerts.triggeredColumnTitle": "実行済み", - "xpack.monitoring.alerts.triggeredColumnValue": "{timestamp} 前", "xpack.monitoring.apm.healthStatusLabel": "ヘルス: {status}", "xpack.monitoring.apm.instance.routeTitle": "{apm} - インスタンス", "xpack.monitoring.apm.instance.status.lastEventDescription": "{timeOfLastEvent} 前", @@ -11074,12 +10997,6 @@ "xpack.monitoring.chart.screenReaderUnaccessibleTitle": "このチャートはスクリーンリーダーではアクセスできません", "xpack.monitoring.chart.seriesScreenReaderListDescription": "間隔: {bucketSize}", "xpack.monitoring.chart.timeSeries.zoomOut": "ズームアウト", - "xpack.monitoring.cluster.listing.alertsInticator.alertsTooltip": "アラート", - "xpack.monitoring.cluster.listing.alertsInticator.clearStatusTooltip": "クラスターステータスはクリアです!", - "xpack.monitoring.cluster.listing.alertsInticator.clearTooltip": "クリア", - "xpack.monitoring.cluster.listing.alertsInticator.highSeverityTooltip": "クラスターにすぐに対処が必要な致命的な問題があります!", - "xpack.monitoring.cluster.listing.alertsInticator.lowSeverityTooltip": "クラスターに低深刻度の問題があります", - "xpack.monitoring.cluster.listing.alertsInticator.mediumSeverityTooltip": "クラスターに影響を及ぼす可能性がある問題があります。", "xpack.monitoring.cluster.listing.dataColumnTitle": "データ", "xpack.monitoring.cluster.listing.incompatibleLicense.getLicenseLinkLabel": "全機能を利用できるライセンスを取得", "xpack.monitoring.cluster.listing.incompatibleLicense.infoMessage": "複数クラスターの監視が必要ですか?{getLicenseInfoLink} して、複数クラスターの監視をご利用ください。", @@ -11102,10 +11019,6 @@ "xpack.monitoring.cluster.listing.standaloneClusterCallOutTitle": "Elasticsearch クラスターに接続されていないインスタンスがあるようです。", "xpack.monitoring.cluster.listing.statusColumnTitle": "ステータス", "xpack.monitoring.cluster.listing.unknownHealthMessage": "不明", - "xpack.monitoring.cluster.overview.alertsPanel.lastCheckedTimeText": "最終確認 {updateDateTime} ({duration} 前に実行)", - "xpack.monitoring.cluster.overview.alertsPanel.severityIconTitle": "{severityIconTitle} ({time} 前に解決)", - "xpack.monitoring.cluster.overview.alertsPanel.topClusterTitle": "トップクラスターアラート", - "xpack.monitoring.cluster.overview.alertsPanel.viewAllButtonLabel": "すべてのアラートを表示", "xpack.monitoring.cluster.overview.apmPanel.apmTitle": "APM", "xpack.monitoring.cluster.overview.apmPanel.instancesTotalLinkAriaLabel": "APM インスタンス: {apmsTotal}", "xpack.monitoring.cluster.overview.apmPanel.lastEventDescription": "{timeOfLastEvent} 前", @@ -11156,8 +11069,6 @@ "xpack.monitoring.cluster.overview.kibanaPanel.overviewLinkAriaLabel": "Kibana の概要", "xpack.monitoring.cluster.overview.kibanaPanel.overviewLinkLabel": "概要", "xpack.monitoring.cluster.overview.kibanaPanel.requestsLabel": "リクエスト", - "xpack.monitoring.cluster.overview.licenseText.expireDateText": "の有効期限は {expiryDate} です", - "xpack.monitoring.cluster.overview.licenseText.toLicensePageLinkLabel": "{licenseType} ライセンス {willExpireOn}", "xpack.monitoring.cluster.overview.logsPanel.logTypeTitle": "{type}", "xpack.monitoring.cluster.overview.logsPanel.noLogsFound": "ログが見つかりませんでした。", "xpack.monitoring.cluster.overview.logstashPanel.betaFeatureTooltip": "ベータ機能", @@ -11371,8 +11282,6 @@ "xpack.monitoring.kibana.instances.metricbeatMigration.detectedNodeDescription": "次のインスタンスは監視されていません。\n 下の「Metricbeat で監視」をクリックして、監視を開始してください。", "xpack.monitoring.kibana.instances.metricbeatMigration.detectedNodeTitle": "Kibana インスタンスが検出されました", "xpack.monitoring.kibana.listing.filterInstancesPlaceholder": "フィルターインスタンス…", - "xpack.monitoring.kibana.listing.instanceStatus.offlineLabel": "オフライン", - "xpack.monitoring.kibana.listing.instanceStatusTitle": "インスタンスステータス: {kibanaStatus}", "xpack.monitoring.kibana.listing.loadAverageColumnTitle": "平均負荷", "xpack.monitoring.kibana.listing.memorySizeColumnTitle": "メモリーサイズ", "xpack.monitoring.kibana.listing.nameColumnTitle": "名前", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 42240203a2eaf..3c8016d64248b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10908,86 +10908,9 @@ "xpack.monitoring.ajaxErrorHandler.requestErrorNotificationTitle": "Monitoring 请求错误", "xpack.monitoring.ajaxErrorHandler.requestFailedNotification.retryButtonLabel": "重试", "xpack.monitoring.ajaxErrorHandler.requestFailedNotificationTitle": "Monitoring 请求失败", - "xpack.monitoring.alertingEmailAddress.description": "用于从 Stack Monitoring 接收告警的默认电子邮件地址", - "xpack.monitoring.alertingEmailAddress.name": "Alerting 电子邮件地址", - "xpack.monitoring.alerts.categoryColumn.generalLabel": "常规", - "xpack.monitoring.alerts.categoryColumnTitle": "类别", - "xpack.monitoring.alerts.clusterAlertsTitle": "集群告警", - "xpack.monitoring.alerts.clusterOverviewLinkLabel": "« 集群概览", - "xpack.monitoring.alerts.clusterState.actionGroups.default": "默认值", - "xpack.monitoring.alerts.clusterStatus.newSubject": "新的 X-Pack Monitoring:集群状态", - "xpack.monitoring.alerts.clusterStatus.redMessage": "分配缺失的主分片和副本分片", - "xpack.monitoring.alerts.clusterStatus.resolvedSubject": "已解决 X-Pack Monitoring:集群状态", - "xpack.monitoring.alerts.clusterStatus.ui.firingMessage": "Elasticsearch 集群状态为 {status}。#start_link{message}#end_link", - "xpack.monitoring.alerts.clusterStatus.ui.resolvedMessage": "Elasticsearch 集群状态为绿色。", - "xpack.monitoring.alerts.clusterStatus.yellowMessage": "分配缺失的副本分片", - "xpack.monitoring.alerts.configuration.confirm": "确认并保存", - "xpack.monitoring.alerts.configuration.createEmailAction": "创建电子邮件操作", - "xpack.monitoring.alerts.configuration.deleteConfiguration.buttonText": "删除", - "xpack.monitoring.alerts.configuration.editConfiguration.buttonText": "编辑", - "xpack.monitoring.alerts.configuration.emailAction.name": "Stack Monitoring 告警的电子邮件操作", - "xpack.monitoring.alerts.configuration.emailAddressLabel": "电子邮件地址", - "xpack.monitoring.alerts.configuration.newActionDropdownDisplay": "创建新电子邮件操作......", - "xpack.monitoring.alerts.configuration.save": "保存", - "xpack.monitoring.alerts.configuration.securityConfigurationError.docsLinkLabel": "文档", - "xpack.monitoring.alerts.configuration.securityConfigurationErrorMessage": "请参阅 {link} 以启用 API 密钥。", - "xpack.monitoring.alerts.configuration.securityConfigurationErrorTitle": "Elasticsearch 中未启用 API 密钥", - "xpack.monitoring.alerts.configuration.selectAction.inputDisplay": "来自:{from},服务:{service}", - "xpack.monitoring.alerts.configuration.selectEmailAction": "选择电子邮件操作", - "xpack.monitoring.alerts.configuration.setEmailAddress": "设置电子邮件以接收告警", - "xpack.monitoring.alerts.configuration.step1.editAction": "在下面编辑操作。", - "xpack.monitoring.alerts.configuration.step1.testingError": "无法发送测试电子邮件。请再次检查您的电子邮件配置。", - "xpack.monitoring.alerts.configuration.step3.saveError": "无法保存", - "xpack.monitoring.alerts.configuration.testConfiguration.buttonText": "测试", - "xpack.monitoring.alerts.configuration.testConfiguration.disabledTooltipText": "请在下面配置电子邮件地址以测试此操作。", - "xpack.monitoring.alerts.configuration.testConfiguration.success": "在我们这边看起来不错!", - "xpack.monitoring.alerts.configuration.unknownError": "出问题了。请查看服务器日志。", - "xpack.monitoring.alerts.filterAlertsPlaceholder": "筛选告警……", - "xpack.monitoring.alerts.highSeverityName": "高", - "xpack.monitoring.alerts.lastCheckedColumnTitle": "上次检查时间", - "xpack.monitoring.alerts.licenseExpiration.actionGroups.default": "默认值", - "xpack.monitoring.alerts.licenseExpiration.newSubject": "新 X-Pack Monitoring:许可证到期", - "xpack.monitoring.alerts.licenseExpiration.resolvedSubject": "已解决 X-Pack Monitoring:许可证到期", "xpack.monitoring.alerts.licenseExpiration.ui.firingMessage": "此集群的许可证将于 #relative后,即 #absolute过期。 #start_link请更新您的许可证。#end_link", "xpack.monitoring.alerts.licenseExpiration.ui.resolvedMessage": "此集群的许可证处于活动状态。", - "xpack.monitoring.alerts.lowSeverityName": "低", - "xpack.monitoring.alerts.mediumSeverityName": "中", - "xpack.monitoring.alerts.messageColumnTitle": "消息", - "xpack.monitoring.alerts.migrate.manageAction.addingNewServiceText": "正在添加新服务......", - "xpack.monitoring.alerts.migrate.manageAction.addNewServiceText": "添加新服务......", - "xpack.monitoring.alerts.migrate.manageAction.cancelLabel": "取消", - "xpack.monitoring.alerts.migrate.manageAction.createLabel": "创建电子邮件操作", - "xpack.monitoring.alerts.migrate.manageAction.fromHelpText": "告警的发件人电子邮件地址", - "xpack.monitoring.alerts.migrate.manageAction.fromText": "发件人", - "xpack.monitoring.alerts.migrate.manageAction.hostHelpText": "服务提供商的主机名", - "xpack.monitoring.alerts.migrate.manageAction.hostText": "主机", - "xpack.monitoring.alerts.migrate.manageAction.passwordHelpText": "用于服务提供商的密码", - "xpack.monitoring.alerts.migrate.manageAction.passwordText": "密码", - "xpack.monitoring.alerts.migrate.manageAction.portHelpText": "服务提供商的端口号", - "xpack.monitoring.alerts.migrate.manageAction.portText": "端口", "xpack.monitoring.alerts.migrate.manageAction.requiredFieldError": "{field} 是必填字段。", - "xpack.monitoring.alerts.migrate.manageAction.saveLabel": "保存电子邮件操作", - "xpack.monitoring.alerts.migrate.manageAction.secureHelpText": "是否将 TLS 用于服务提供商", - "xpack.monitoring.alerts.migrate.manageAction.secureText": "安全", - "xpack.monitoring.alerts.migrate.manageAction.serviceHelpText": "了解详情", - "xpack.monitoring.alerts.migrate.manageAction.serviceText": "服务", - "xpack.monitoring.alerts.migrate.manageAction.userHelpText": "用于服务提供商的用户", - "xpack.monitoring.alerts.migrate.manageAction.userText": "用户", - "xpack.monitoring.alerts.notResolvedDescription": "未解决", - "xpack.monitoring.alerts.resolvedAgoDescription": "{duration}前", - "xpack.monitoring.alerts.resolvedColumnTitle": "已解决", - "xpack.monitoring.alerts.severityTitle": "{severity}紧急告警", - "xpack.monitoring.alerts.severityTitle.unknown": "未知", - "xpack.monitoring.alerts.severityValue.unknown": "不可用", - "xpack.monitoring.alerts.status.flyoutSubtitle": "配置电子邮件服务器和电子邮件地址以接收告警。", - "xpack.monitoring.alerts.status.flyoutTitle": "Monitoring 告警", - "xpack.monitoring.alerts.status.manage": "想要进行更改?单击此处。", - "xpack.monitoring.alerts.status.needToMigrate": "将集群告警迁移到我们新的告警平台。", - "xpack.monitoring.alerts.status.needToMigrateTitle": "嘿!我们已优化 Alerting!", - "xpack.monitoring.alerts.status.upToDate": "Kibana Alerting 与时俱进!", - "xpack.monitoring.alerts.statusColumnTitle": "状态", - "xpack.monitoring.alerts.triggeredColumnTitle": "已触发", - "xpack.monitoring.alerts.triggeredColumnValue": "{timestamp}前", "xpack.monitoring.apm.healthStatusLabel": "运行状况:{status}", "xpack.monitoring.apm.instance.routeTitle": "{apm} - 实例", "xpack.monitoring.apm.instance.status.lastEventDescription": "{timeOfLastEvent}前", @@ -11080,12 +11003,6 @@ "xpack.monitoring.chart.screenReaderUnaccessibleTitle": "此图表不支持屏幕阅读器读取", "xpack.monitoring.chart.seriesScreenReaderListDescription": "时间间隔:{bucketSize}", "xpack.monitoring.chart.timeSeries.zoomOut": "缩小", - "xpack.monitoring.cluster.listing.alertsInticator.alertsTooltip": "告警", - "xpack.monitoring.cluster.listing.alertsInticator.clearStatusTooltip": "集群状态正常!", - "xpack.monitoring.cluster.listing.alertsInticator.clearTooltip": "清除", - "xpack.monitoring.cluster.listing.alertsInticator.highSeverityTooltip": "有一些紧急集群问题需要您立即关注!", - "xpack.monitoring.cluster.listing.alertsInticator.lowSeverityTooltip": "存在一些低紧急集群问题", - "xpack.monitoring.cluster.listing.alertsInticator.mediumSeverityTooltip": "有一些问题可能影响您的集群。", "xpack.monitoring.cluster.listing.dataColumnTitle": "数据", "xpack.monitoring.cluster.listing.incompatibleLicense.getLicenseLinkLabel": "获取具有完整功能的许可证", "xpack.monitoring.cluster.listing.incompatibleLicense.infoMessage": "需要监测多个集群?{getLicenseInfoLink}以实现多集群监测。", @@ -11108,10 +11025,6 @@ "xpack.monitoring.cluster.listing.standaloneClusterCallOutTitle": "似乎您具有未连接到 Elasticsearch 集群的实例。", "xpack.monitoring.cluster.listing.statusColumnTitle": "状态", "xpack.monitoring.cluster.listing.unknownHealthMessage": "未知", - "xpack.monitoring.cluster.overview.alertsPanel.lastCheckedTimeText": "上次检查时间是 {updateDateTime}(触发于 {duration}前)", - "xpack.monitoring.cluster.overview.alertsPanel.severityIconTitle": "{severityIconTitle}(已在 {time}前解决)", - "xpack.monitoring.cluster.overview.alertsPanel.topClusterTitle": "最亟需处理的集群告警", - "xpack.monitoring.cluster.overview.alertsPanel.viewAllButtonLabel": "查看所有告警", "xpack.monitoring.cluster.overview.apmPanel.apmTitle": "APM", "xpack.monitoring.cluster.overview.apmPanel.instancesTotalLinkAriaLabel": "APM 实例:{apmsTotal}", "xpack.monitoring.cluster.overview.apmPanel.lastEventDescription": "{timeOfLastEvent}前", @@ -11162,8 +11075,6 @@ "xpack.monitoring.cluster.overview.kibanaPanel.overviewLinkAriaLabel": "Kibana 概览", "xpack.monitoring.cluster.overview.kibanaPanel.overviewLinkLabel": "概览", "xpack.monitoring.cluster.overview.kibanaPanel.requestsLabel": "请求", - "xpack.monitoring.cluster.overview.licenseText.expireDateText": "将于 {expiryDate}过期", - "xpack.monitoring.cluster.overview.licenseText.toLicensePageLinkLabel": "{licenseType}许可{willExpireOn}", "xpack.monitoring.cluster.overview.logsPanel.logTypeTitle": "{type}", "xpack.monitoring.cluster.overview.logsPanel.noLogsFound": "未找到任何日志。", "xpack.monitoring.cluster.overview.logstashPanel.betaFeatureTooltip": "公测版功能", @@ -11377,8 +11288,6 @@ "xpack.monitoring.kibana.instances.metricbeatMigration.detectedNodeDescription": "以下实例未受监测。\n 单击下面的“使用 Metricbeat 监测”以开始监测。", "xpack.monitoring.kibana.instances.metricbeatMigration.detectedNodeTitle": "检测到 Kibana 实例", "xpack.monitoring.kibana.listing.filterInstancesPlaceholder": "筛选实例……", - "xpack.monitoring.kibana.listing.instanceStatus.offlineLabel": "脱机", - "xpack.monitoring.kibana.listing.instanceStatusTitle": "实例状态:{kibanaStatus}", "xpack.monitoring.kibana.listing.loadAverageColumnTitle": "负载平均值", "xpack.monitoring.kibana.listing.memorySizeColumnTitle": "内存大小", "xpack.monitoring.kibana.listing.nameColumnTitle": "名称", diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index a0e8f3583ac43..55653f49001b9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -10,6 +10,7 @@ import { Plugin } from './plugin'; export { AlertsContextProvider } from './application/context/alerts_context'; export { ActionsConnectorsContextProvider } from './application/context/actions_connectors_context'; export { AlertAdd } from './application/sections/alert_form'; +export { AlertEdit } from './application/sections'; export { ActionForm } from './application/sections/action_connector_form'; export { AlertAction, diff --git a/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/multicluster.json b/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/multicluster.json index 50614ca64bbd5..b7c3aee5471d7 100644 --- a/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/multicluster.json +++ b/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/multicluster.json @@ -107,7 +107,8 @@ "clusterMeta": { "enabled": false, "message": "Cluster [clustertwo] license type [basic] does not support Cluster Alerts" - } + }, + "list": {} }, "isPrimary": false, "status": "green", @@ -219,10 +220,7 @@ "alertsMeta": { "enabled": true }, - "count": 1, - "low": 0, - "medium": 1, - "high": 0 + "list": {} }, "isPrimary": false, "status": "yellow", @@ -333,7 +331,8 @@ "alerts": { "alertsMeta": { "enabled": true - } + }, + "list": {} }, "isPrimary": false, "status": "green", diff --git a/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/overview.json b/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/overview.json index 49e80b244f760..15ff905478933 100644 --- a/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/overview.json +++ b/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/overview.json @@ -114,22 +114,6 @@ "total": null } }, - "alerts": [{ - "metadata": { - "severity": 1100, - "cluster_uuid": "y1qOsQPiRrGtmdEuM3APJw", - "version_created": 6000026, - "watch": "elasticsearch_cluster_status", - "link": "elasticsearch/indices", - "alert_index": ".monitoring-alerts-6", - "type": "monitoring" - }, - "update_timestamp": "2017-08-23T21:45:31.882Z", - "prefix": "Elasticsearch cluster status is yellow.", - "message": "Allocate missing replica shards.", - "resolved_timestamp": "2017-08-23T21:45:31.882Z", - "timestamp": "2017-08-23T21:28:25.639Z" - }], "isCcrEnabled": true, "isPrimary": true, "status": "green" diff --git a/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/cluster.json b/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/cluster.json index 802bd0c7fcd74..f0fe8c152b49f 100644 --- a/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/cluster.json +++ b/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/cluster.json @@ -45,8 +45,5 @@ "total": 0 } }, - "alerts": { - "message": "Cluster Alerts are not displayed because the [production] cluster's license could not be determined." - }, "isPrimary": false }] diff --git a/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/clusters.json b/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/clusters.json index 68cfe51fbcb95..f938479578801 100644 --- a/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/clusters.json +++ b/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/clusters.json @@ -107,7 +107,8 @@ "clusterMeta": { "enabled": false, "message": "Cluster [monitoring] license type [basic] does not support Cluster Alerts" - } + }, + "list": {} }, "isPrimary": true, "status": "yellow", @@ -174,7 +175,8 @@ "clusterMeta": { "enabled": false, "message": "Cluster [] license type [undefined] does not support Cluster Alerts" - } + }, + "list": {} }, "isPrimary": false, "isCcrEnabled": false diff --git a/x-pack/test/functional/apps/monitoring/cluster/alerts.js b/x-pack/test/functional/apps/monitoring/cluster/alerts.js deleted file mode 100644 index 2636fc5028068..0000000000000 --- a/x-pack/test/functional/apps/monitoring/cluster/alerts.js +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { getLifecycleMethods } from '../_get_lifecycle_methods'; - -const HIGH_ALERT_MESSAGE = 'High severity alert'; -const MEDIUM_ALERT_MESSAGE = 'Medium severity alert'; -const LOW_ALERT_MESSAGE = 'Low severity alert'; - -export default function ({ getService, getPageObjects }) { - const PageObjects = getPageObjects(['monitoring', 'header']); - const overview = getService('monitoringClusterOverview'); - const alerts = getService('monitoringClusterAlerts'); - const indices = getService('monitoringElasticsearchIndices'); - - describe('Cluster alerts', () => { - describe('cluster has single alert', () => { - const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects); - - before(async () => { - await setup('monitoring/singlecluster-yellow-platinum', { - from: 'Aug 29, 2017 @ 17:23:47.528', - to: 'Aug 29, 2017 @ 17:25:50.701', - }); - - // ensure cluster alerts are shown on overview - expect(await overview.doesClusterAlertsExist()).to.be(true); - }); - - after(async () => { - await tearDown(); - }); - - it('in alerts panel, a single medium alert is shown', async () => { - const clusterAlerts = await alerts.getOverviewAlerts(); - await new Promise((r) => setTimeout(r, 10000)); - expect(clusterAlerts.length).to.be(1); - - const { alertIcon, alertText } = await alerts.getOverviewAlert(0); - expect(alertIcon).to.be(MEDIUM_ALERT_MESSAGE); - expect(alertText).to.be( - 'Elasticsearch cluster status is yellow. Allocate missing replica shards.' - ); - }); - }); - - describe('cluster has 10 alerts', () => { - const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects); - - before(async () => { - await setup('monitoring/singlecluster-yellow-platinum--with-10-alerts', { - from: 'Aug 29, 2017 @ 17:23:47.528', - to: 'Aug 29, 2017 @ 17:25:50.701', - }); - - // ensure cluster alerts are shown on overview - expect(await overview.doesClusterAlertsExist()).to.be(true); - }); - - after(async () => { - await tearDown(); - }); - - it('in alerts panel, top 3 alerts are shown', async () => { - const clusterAlerts = await alerts.getOverviewAlerts(); - expect(clusterAlerts.length).to.be(3); - - // check the all data in the panel - const panelData = [ - { - alertIcon: HIGH_ALERT_MESSAGE, - alertText: - 'One cannot step twice in the same river. Heraclitus (ca. 540 – ca. 480 BCE)', - }, - { - alertIcon: HIGH_ALERT_MESSAGE, - alertText: 'Quality is not an act, it is a habit. Aristotle (384-322 BCE)', - }, - { - alertIcon: HIGH_ALERT_MESSAGE, - alertText: - 'Life contains but two tragedies. One is not to get your heart’s desire; the other is to get it. Socrates (470-399 BCE)', - }, - ]; - - const alertsAll = await alerts.getOverviewAlertsAll(); - - alertsAll.forEach((obj, index) => { - expect(alertsAll[index].alertIcon).to.be(panelData[index].alertIcon); - expect(alertsAll[index].alertText).to.be(panelData[index].alertText); - }); - }); - - it('in alerts table view, all alerts are shown', async () => { - await alerts.clickViewAll(); - expect(await alerts.isOnListingPage()).to.be(true); - - // Check the all data in the table - const tableData = [ - { - alertIcon: HIGH_ALERT_MESSAGE, - alertText: - 'One cannot step twice in the same river. Heraclitus (ca. 540 – ca. 480 BCE)', - }, - { - alertIcon: HIGH_ALERT_MESSAGE, - alertText: 'Quality is not an act, it is a habit. Aristotle (384-322 BCE)', - }, - { - alertIcon: HIGH_ALERT_MESSAGE, - alertText: - 'Life contains but two tragedies. One is not to get your heart’s desire; the other is to get it. Socrates (470-399 BCE)', - }, - { - alertIcon: HIGH_ALERT_MESSAGE, - alertText: - 'The owl of Minerva spreads its wings only with the falling of the dusk. G.W.F. Hegel (1770 – 1831)', - }, - { - alertIcon: MEDIUM_ALERT_MESSAGE, - alertText: - 'We live in the best of all possible worlds. Gottfried Wilhelm Leibniz (1646 – 1716)', - }, - { - alertIcon: MEDIUM_ALERT_MESSAGE, - alertText: - 'To be is to be perceived (Esse est percipi). Bishop George Berkeley (1685 – 1753)', - }, - { - alertIcon: MEDIUM_ALERT_MESSAGE, - alertText: 'I think therefore I am. René Descartes (1596 – 1650)', - }, - { - alertIcon: LOW_ALERT_MESSAGE, - alertText: - 'The life of man [is] solitary, poor, nasty, brutish, and short. Thomas Hobbes (1588 – 1679)', - }, - { - alertIcon: LOW_ALERT_MESSAGE, - alertText: - 'Entities should not be multiplied unnecessarily. William of Ockham (1285 - 1349?)', - }, - { - alertIcon: LOW_ALERT_MESSAGE, - alertText: 'The unexamined life is not worth living. Socrates (470-399 BCE)', - }, - ]; - - // In some environments, with Elasticsearch 7, the cluster's status goes yellow, which makes - // this test flakey, as there is occasionally an unexpected alert about this. So, we'll ignore - // that one. - const alertsAll = Array.from(await alerts.getTableAlertsAll()).filter( - ({ alertText }) => !alertText.includes('status is yellow') - ); - expect(alertsAll.length).to.be(tableData.length); - - alertsAll.forEach((obj, index) => { - expect(`${alertsAll[index].alertIcon} ${alertsAll[index].alertText}`).to.be( - `${tableData[index].alertIcon} ${tableData[index].alertText}` - ); - }); - - await PageObjects.monitoring.clickBreadcrumb('~breadcrumbClusters'); - }); - }); - - describe('alert actions take you to the elasticsearch indices listing', () => { - const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects); - - before(async () => { - await setup('monitoring/singlecluster-yellow-platinum', { - from: 'Aug 29, 2017 @ 17:23:47.528', - to: 'Aug 29, 2017 @ 17:25:50.701', - }); - - // ensure cluster alerts are shown on overview - expect(await overview.doesClusterAlertsExist()).to.be(true); - }); - - after(async () => { - await tearDown(); - }); - - it('with alert on overview', async () => { - const { alertAction } = await alerts.getOverviewAlert(0); - await alertAction.click(); - expect(await indices.isOnListing()).to.be(true); - - await PageObjects.monitoring.clickBreadcrumb('~breadcrumbClusters'); - }); - - it('with alert on listing table page', async () => { - await alerts.clickViewAll(); - expect(await alerts.isOnListingPage()).to.be(true); - - const { alertAction } = await alerts.getTableAlert(0); - await alertAction.click(); - expect(await indices.isOnListing()).to.be(true); - - await PageObjects.monitoring.clickBreadcrumb('~breadcrumbClusters'); - }); - }); - }); -} diff --git a/x-pack/test/functional/apps/monitoring/cluster/overview.js b/x-pack/test/functional/apps/monitoring/cluster/overview.js index 3396426e95380..0e608e9a055fa 100644 --- a/x-pack/test/functional/apps/monitoring/cluster/overview.js +++ b/x-pack/test/functional/apps/monitoring/cluster/overview.js @@ -25,10 +25,6 @@ export default function ({ getService, getPageObjects }) { await tearDown(); }); - it('shows alerts panel, because there are resolved alerts in the time range', async () => { - expect(await overview.doesClusterAlertsExist()).to.be(true); - }); - it('elasticsearch panel has no ML line, because license is Gold', async () => { expect(await overview.doesEsMlJobsExist()).to.be(false); }); @@ -80,10 +76,6 @@ export default function ({ getService, getPageObjects }) { await tearDown(); }); - it('shows alerts panel, because cluster status is Yellow', async () => { - expect(await overview.doesClusterAlertsExist()).to.be(true); - }); - it('elasticsearch panel has ML, because license is Platinum', async () => { expect(await overview.getEsMlJobs()).to.be('0'); }); diff --git a/x-pack/test/functional/apps/monitoring/index.js b/x-pack/test/functional/apps/monitoring/index.js index 77ca4087da13a..c383d8593a4fa 100644 --- a/x-pack/test/functional/apps/monitoring/index.js +++ b/x-pack/test/functional/apps/monitoring/index.js @@ -12,7 +12,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./cluster/list')); loadTestFile(require.resolve('./cluster/overview')); - loadTestFile(require.resolve('./cluster/alerts')); // loadTestFile(require.resolve('./cluster/license')); loadTestFile(require.resolve('./elasticsearch/overview')); diff --git a/x-pack/test/functional/services/monitoring/elasticsearch_nodes.js b/x-pack/test/functional/services/monitoring/elasticsearch_nodes.js index 8b0ddda8859b8..0cae469e01697 100644 --- a/x-pack/test/functional/services/monitoring/elasticsearch_nodes.js +++ b/x-pack/test/functional/services/monitoring/elasticsearch_nodes.js @@ -19,12 +19,12 @@ export function MonitoringElasticsearchNodesProvider({ getService, getPageObject const SUBJ_SEARCH_BAR = `${SUBJ_TABLE_CONTAINER} > monitoringTableToolBar`; const SUBJ_TABLE_SORT_NAME_COL = `tableHeaderCell_name_0`; - const SUBJ_TABLE_SORT_STATUS_COL = `tableHeaderCell_isOnline_1`; - const SUBJ_TABLE_SORT_SHARDS_COL = `tableHeaderCell_shardCount_2`; - const SUBJ_TABLE_SORT_CPU_COL = `tableHeaderCell_node_cpu_utilization_3`; - const SUBJ_TABLE_SORT_LOAD_COL = `tableHeaderCell_node_load_average_4`; - const SUBJ_TABLE_SORT_MEM_COL = `tableHeaderCell_node_jvm_mem_percent_5`; - const SUBJ_TABLE_SORT_DISK_COL = `tableHeaderCell_node_free_space_6`; + const SUBJ_TABLE_SORT_STATUS_COL = `tableHeaderCell_isOnline_2`; + const SUBJ_TABLE_SORT_SHARDS_COL = `tableHeaderCell_shardCount_3`; + const SUBJ_TABLE_SORT_CPU_COL = `tableHeaderCell_node_cpu_utilization_4`; + const SUBJ_TABLE_SORT_LOAD_COL = `tableHeaderCell_node_load_average_5`; + const SUBJ_TABLE_SORT_MEM_COL = `tableHeaderCell_node_jvm_mem_percent_6`; + const SUBJ_TABLE_SORT_DISK_COL = `tableHeaderCell_node_free_space_7`; const SUBJ_TABLE_BODY = 'elasticsearchNodesTableContainer'; const SUBJ_NODES_NAMES = `${SUBJ_TABLE_BODY} > name`; From 8ecbb25ab5ea15f9573536bb17db41b7988a8186 Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Tue, 14 Jul 2020 15:57:22 -0600 Subject: [PATCH 49/82] [expressions] AST Builder (#64395) --- ...blic.esaggsexpressionfunctiondefinition.md | 11 + .../kibana-plugin-plugins-data-public.md | 1 + ...rver.esaggsexpressionfunctiondefinition.md | 11 + .../kibana-plugin-plugins-data-server.md | 1 + src/plugins/data/common/index.ts | 1 + .../data/common/search/expressions/esaggs.ts | 43 ++ .../data/common/search/expressions/index.ts | 20 + src/plugins/data/public/index.ts | 2 +- src/plugins/data/public/public.api.md | 13 +- .../data/public/search/expressions/esaggs.ts | 23 +- src/plugins/data/server/index.ts | 2 +- src/plugins/data/server/server.api.md | 11 + .../common/ast/build_expression.test.ts | 386 +++++++++++++++++ .../common/ast/build_expression.ts | 169 ++++++++ .../common/ast/build_function.test.ts | 399 ++++++++++++++++++ .../expressions/common/ast/build_function.ts | 243 +++++++++++ .../expressions/common/ast/format.test.ts | 18 +- src/plugins/expressions/common/ast/format.ts | 10 +- .../common/ast/format_expression.test.ts | 39 ++ .../common/ast/format_expression.ts | 30 ++ src/plugins/expressions/common/ast/index.ts | 9 +- .../expressions/common/ast/parse.test.ts | 6 + src/plugins/expressions/common/ast/parse.ts | 8 +- .../common/ast/parse_expression.ts | 2 +- .../common/expression_functions/specs/clog.ts | 4 +- .../common/expression_functions/specs/font.ts | 4 +- .../common/expression_functions/specs/var.ts | 7 +- .../expression_functions/specs/var_set.ts | 9 +- .../common/expression_functions/types.ts | 33 +- src/plugins/expressions/public/index.ts | 6 + src/plugins/expressions/server/index.ts | 6 + 31 files changed, 1478 insertions(+), 49 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esaggsexpressionfunctiondefinition.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esaggsexpressionfunctiondefinition.md create mode 100644 src/plugins/data/common/search/expressions/esaggs.ts create mode 100644 src/plugins/data/common/search/expressions/index.ts create mode 100644 src/plugins/expressions/common/ast/build_expression.test.ts create mode 100644 src/plugins/expressions/common/ast/build_expression.ts create mode 100644 src/plugins/expressions/common/ast/build_function.test.ts create mode 100644 src/plugins/expressions/common/ast/build_function.ts create mode 100644 src/plugins/expressions/common/ast/format_expression.test.ts create mode 100644 src/plugins/expressions/common/ast/format_expression.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esaggsexpressionfunctiondefinition.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esaggsexpressionfunctiondefinition.md new file mode 100644 index 0000000000000..6cf05dde27627 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esaggsexpressionfunctiondefinition.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [EsaggsExpressionFunctionDefinition](./kibana-plugin-plugins-data-public.esaggsexpressionfunctiondefinition.md) + +## EsaggsExpressionFunctionDefinition type + +Signature: + +```typescript +export declare type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'esaggs', Input, Arguments, Output>; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 7cb6ef64431bf..4852ad15781c7 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -125,6 +125,7 @@ | [AggGroupName](./kibana-plugin-plugins-data-public.agggroupname.md) | | | [AggParam](./kibana-plugin-plugins-data-public.aggparam.md) | | | [CustomFilter](./kibana-plugin-plugins-data-public.customfilter.md) | | +| [EsaggsExpressionFunctionDefinition](./kibana-plugin-plugins-data-public.esaggsexpressionfunctiondefinition.md) | | | [EsQuerySortValue](./kibana-plugin-plugins-data-public.esquerysortvalue.md) | | | [ExistsFilter](./kibana-plugin-plugins-data-public.existsfilter.md) | | | [FieldFormatId](./kibana-plugin-plugins-data-public.fieldformatid.md) | id type is needed for creating custom converters. | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esaggsexpressionfunctiondefinition.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esaggsexpressionfunctiondefinition.md new file mode 100644 index 0000000000000..572c4e0c1eb2f --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esaggsexpressionfunctiondefinition.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [EsaggsExpressionFunctionDefinition](./kibana-plugin-plugins-data-server.esaggsexpressionfunctiondefinition.md) + +## EsaggsExpressionFunctionDefinition type + +Signature: + +```typescript +export declare type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'esaggs', Input, Arguments, Output>; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index 9adefda718338..6bf481841f334 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -69,6 +69,7 @@ | Type Alias | Description | | --- | --- | +| [EsaggsExpressionFunctionDefinition](./kibana-plugin-plugins-data-server.esaggsexpressionfunctiondefinition.md) | | | [FieldFormatsGetConfigFn](./kibana-plugin-plugins-data-server.fieldformatsgetconfigfn.md) | | | [IFieldFormatsRegistry](./kibana-plugin-plugins-data-server.ifieldformatsregistry.md) | | | [ParsedInterval](./kibana-plugin-plugins-data-server.parsedinterval.md) | | diff --git a/src/plugins/data/common/index.ts b/src/plugins/data/common/index.ts index 0fb45fcc739d4..ca6bc965d48c5 100644 --- a/src/plugins/data/common/index.ts +++ b/src/plugins/data/common/index.ts @@ -26,5 +26,6 @@ export * from './kbn_field_types'; export * from './query'; export * from './search'; export * from './search/aggs'; +export * from './search/expressions'; export * from './types'; export * from './utils'; diff --git a/src/plugins/data/common/search/expressions/esaggs.ts b/src/plugins/data/common/search/expressions/esaggs.ts new file mode 100644 index 0000000000000..2957512886b4d --- /dev/null +++ b/src/plugins/data/common/search/expressions/esaggs.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + KibanaContext, + KibanaDatatable, + ExpressionFunctionDefinition, +} from '../../../../../plugins/expressions/common'; + +type Input = KibanaContext | null; +type Output = Promise; + +interface Arguments { + index: string; + metricsAtAllLevels: boolean; + partialRows: boolean; + includeFormatHints: boolean; + aggConfigs: string; + timeFields?: string[]; +} + +export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition< + 'esaggs', + Input, + Arguments, + Output +>; diff --git a/src/plugins/data/common/search/expressions/index.ts b/src/plugins/data/common/search/expressions/index.ts new file mode 100644 index 0000000000000..f1a39a8383629 --- /dev/null +++ b/src/plugins/data/common/search/expressions/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './esaggs'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 2efd1c82aae79..6328e694193c9 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -313,7 +313,7 @@ import { toAbsoluteDates, } from '../common'; -export { ParsedInterval } from '../common'; +export { EsaggsExpressionFunctionDefinition, ParsedInterval } from '../common'; export { // aggs diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 0c23ba340304f..cd3fff010c053 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -52,6 +52,7 @@ import { EuiButtonEmptyProps } from '@elastic/eui'; import { EuiComboBoxProps } from '@elastic/eui'; import { EuiConfirmModalProps } from '@elastic/eui'; import { EuiGlobalToastListToast } from '@elastic/eui'; +import { EventEmitter } from 'events'; import { ExclusiveUnion } from '@elastic/eui'; import { ExistsParams } from 'elasticsearch'; import { ExplainParams } from 'elasticsearch'; @@ -145,7 +146,7 @@ import { ReindexParams } from 'elasticsearch'; import { ReindexRethrottleParams } from 'elasticsearch'; import { RenderSearchTemplateParams } from 'elasticsearch'; import { RequestAdapter } from 'src/plugins/inspector/common'; -import { RequestStatistics } from 'src/plugins/inspector/common'; +import { RequestStatistics as RequestStatistics_2 } from 'src/plugins/inspector/common'; import { Required } from '@kbn/utility-types'; import * as Rx from 'rxjs'; import { SavedObject } from 'src/core/server'; @@ -180,6 +181,7 @@ import { UiActionsSetup } from 'src/plugins/ui_actions/public'; import { UiActionsStart } from 'src/plugins/ui_actions/public'; import { Unit } from '@elastic/datemath'; import { UnregisterCallback } from 'history'; +import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; import { UpdateDocumentByQueryParams } from 'elasticsearch'; import { UpdateDocumentParams } from 'elasticsearch'; import { UserProvidedValues } from 'src/core/server/types'; @@ -425,6 +427,15 @@ export enum ES_FIELD_TYPES { // @public (undocumented) export const ES_SEARCH_STRATEGY = "es"; +// Warning: (ae-forgotten-export) The symbol "ExpressionFunctionDefinition" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Input" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Arguments" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Output" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "EsaggsExpressionFunctionDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'esaggs', Input, Arguments, Output>; + // Warning: (ae-missing-release-tag) "esFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index 4ac6c823d2e3b..b01f17762b2be 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -19,12 +19,8 @@ import { get, hasIn } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { - KibanaContext, - KibanaDatatable, - ExpressionFunctionDefinition, - KibanaDatatableColumn, -} from 'src/plugins/expressions/public'; + +import { KibanaDatatable, KibanaDatatableColumn } from 'src/plugins/expressions/public'; import { calculateObjectHash } from '../../../../../plugins/kibana_utils/public'; import { PersistedState } from '../../../../../plugins/visualizations/public'; import { Adapters } from '../../../../../plugins/inspector/public'; @@ -34,6 +30,7 @@ import { ISearchSource } from '../search_source'; import { tabifyAggResponse } from '../tabify'; import { calculateBounds, + EsaggsExpressionFunctionDefinition, Filter, getTime, IIndexPattern, @@ -71,18 +68,6 @@ export interface RequestHandlerParams { const name = 'esaggs'; -type Input = KibanaContext | null; -type Output = Promise; - -interface Arguments { - index: string; - metricsAtAllLevels: boolean; - partialRows: boolean; - includeFormatHints: boolean; - aggConfigs: string; - timeFields?: string[]; -} - const handleCourierRequest = async ({ searchSource, aggs, @@ -244,7 +229,7 @@ const handleCourierRequest = async ({ return (searchSource as any).tabifiedResponse; }; -export const esaggs = (): ExpressionFunctionDefinition => ({ +export const esaggs = (): EsaggsExpressionFunctionDefinition => ({ name, type: 'kibana_datatable', inputTypes: ['kibana_context', 'null'], diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 321bd913ce760..461b21e1cc980 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -161,7 +161,7 @@ import { toAbsoluteDates, } from '../common'; -export { ParsedInterval } from '../common'; +export { EsaggsExpressionFunctionDefinition, ParsedInterval } from '../common'; export { ISearchStrategy, diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 88f2cc3264c6e..4dc60056ed918 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -39,6 +39,7 @@ import { DeleteTemplateParams } from 'elasticsearch'; import { DetailedPeerCertificate } from 'tls'; import { Duration } from 'moment'; import { ErrorToastOptions } from 'src/core/public/notifications'; +import { EventEmitter } from 'events'; import { ExistsParams } from 'elasticsearch'; import { ExplainParams } from 'elasticsearch'; import { FieldStatsParams } from 'elasticsearch'; @@ -146,6 +147,7 @@ import { ToastInputFields } from 'src/core/public/notifications'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; import { Unit } from '@elastic/datemath'; +import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; import { UpdateDocumentByQueryParams } from 'elasticsearch'; import { UpdateDocumentParams } from 'elasticsearch'; import { Url } from 'url'; @@ -220,6 +222,15 @@ export enum ES_FIELD_TYPES { _TYPE = "_type" } +// Warning: (ae-forgotten-export) The symbol "ExpressionFunctionDefinition" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Input" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Arguments" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Output" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "EsaggsExpressionFunctionDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'esaggs', Input, Arguments, Output>; + // Warning: (ae-missing-release-tag) "esFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/src/plugins/expressions/common/ast/build_expression.test.ts b/src/plugins/expressions/common/ast/build_expression.test.ts new file mode 100644 index 0000000000000..657b9d3bdda28 --- /dev/null +++ b/src/plugins/expressions/common/ast/build_expression.test.ts @@ -0,0 +1,386 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExpressionAstExpression } from './types'; +import { buildExpression, isExpressionAstBuilder, isExpressionAst } from './build_expression'; +import { buildExpressionFunction, ExpressionAstFunctionBuilder } from './build_function'; +import { format } from './format'; + +describe('isExpressionAst()', () => { + test('returns true when a valid AST is provided', () => { + const ast = { + type: 'expression', + chain: [ + { + type: 'function', + function: 'foo', + arguments: {}, + }, + ], + }; + expect(isExpressionAst(ast)).toBe(true); + }); + + test('returns false when a invalid value is provided', () => { + const invalidValues = [ + buildExpression('hello | world'), + false, + null, + undefined, + 'hi', + { type: 'unknown' }, + {}, + ]; + + invalidValues.forEach((value) => { + expect(isExpressionAst(value)).toBe(false); + }); + }); +}); + +describe('isExpressionAstBuilder()', () => { + test('returns true when a valid builder is provided', () => { + const builder = buildExpression('hello | world'); + expect(isExpressionAstBuilder(builder)).toBe(true); + }); + + test('returns false when a invalid value is provided', () => { + const invalidValues = [ + buildExpressionFunction('myFn', {}), + false, + null, + undefined, + 'hi', + { type: 'unknown' }, + {}, + ]; + + invalidValues.forEach((value) => { + expect(isExpressionAstBuilder(value)).toBe(false); + }); + }); +}); + +describe('buildExpression()', () => { + let ast: ExpressionAstExpression; + let str: string; + + beforeEach(() => { + ast = { + type: 'expression', + chain: [ + { + type: 'function', + function: 'foo', + arguments: { + bar: ['baz'], + subexp: [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'hello', + arguments: { + world: [false, true], + }, + }, + ], + }, + ], + }, + }, + ], + }; + str = format(ast, 'expression'); + }); + + test('accepts an expression AST as input', () => { + ast = { + type: 'expression', + chain: [ + { + type: 'function', + function: 'foo', + arguments: { + bar: ['baz'], + }, + }, + ], + }; + const exp = buildExpression(ast); + expect(exp.toAst()).toEqual(ast); + }); + + test('converts subexpressions in provided AST to expression builder instances', () => { + const exp = buildExpression(ast); + expect(isExpressionAstBuilder(exp.functions[0].getArgument('subexp')![0])).toBe(true); + }); + + test('accepts an expresssion string as input', () => { + const exp = buildExpression(str); + expect(exp.toAst()).toEqual(ast); + }); + + test('accepts an array of function builders as input', () => { + const firstFn = ast.chain[0]; + const exp = buildExpression([ + buildExpressionFunction(firstFn.function, firstFn.arguments), + buildExpressionFunction('hiya', {}), + ]); + expect(exp.toAst()).toMatchInlineSnapshot(` + Object { + "chain": Array [ + Object { + "arguments": Object { + "bar": Array [ + "baz", + ], + "subexp": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "world": Array [ + false, + true, + ], + }, + "function": "hello", + "type": "function", + }, + ], + "type": "expression", + }, + ], + }, + "function": "foo", + "type": "function", + }, + Object { + "arguments": Object {}, + "function": "hiya", + "type": "function", + }, + ], + "type": "expression", + } + `); + }); + + describe('functions', () => { + test('returns an array of buildExpressionFunctions', () => { + const exp = buildExpression(ast); + expect(exp.functions).toHaveLength(1); + expect(exp.functions.map((f) => f.name)).toEqual(['foo']); + }); + + test('functions.push() adds new function to the AST', () => { + const exp = buildExpression(ast); + const fn = buildExpressionFunction('test', { abc: [123] }); + exp.functions.push(fn); + expect(exp.toAst()).toMatchInlineSnapshot(` + Object { + "chain": Array [ + Object { + "arguments": Object { + "bar": Array [ + "baz", + ], + "subexp": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "world": Array [ + false, + true, + ], + }, + "function": "hello", + "type": "function", + }, + ], + "type": "expression", + }, + ], + }, + "function": "foo", + "type": "function", + }, + Object { + "arguments": Object { + "abc": Array [ + 123, + ], + }, + "function": "test", + "type": "function", + }, + ], + "type": "expression", + } + `); + }); + + test('functions can be reordered', () => { + const exp = buildExpression(ast); + const fn = buildExpressionFunction('test', { abc: [123] }); + exp.functions.push(fn); + expect(exp.functions.map((f) => f.name)).toEqual(['foo', 'test']); + const testFn = exp.functions[1]; + exp.functions[1] = exp.functions[0]; + exp.functions[0] = testFn; + expect(exp.functions.map((f) => f.name)).toEqual(['test', 'foo']); + const barFn = buildExpressionFunction('bar', {}); + const fooFn = exp.functions[1]; + exp.functions[1] = barFn; + exp.functions[2] = fooFn; + expect(exp.functions.map((f) => f.name)).toEqual(['test', 'bar', 'foo']); + }); + + test('functions can be removed', () => { + const exp = buildExpression(ast); + const fn = buildExpressionFunction('test', { abc: [123] }); + exp.functions.push(fn); + expect(exp.functions.map((f) => f.name)).toEqual(['foo', 'test']); + exp.functions.shift(); + expect(exp.functions.map((f) => f.name)).toEqual(['test']); + }); + }); + + describe('#toAst', () => { + test('generates the AST for an expression', () => { + const exp = buildExpression('foo | bar hello=true hello=false'); + expect(exp.toAst()).toMatchInlineSnapshot(` + Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "foo", + "type": "function", + }, + Object { + "arguments": Object { + "hello": Array [ + true, + false, + ], + }, + "function": "bar", + "type": "function", + }, + ], + "type": "expression", + } + `); + }); + + test('throws when called on an expression with no functions', () => { + ast.chain = []; + const exp = buildExpression(ast); + expect(() => { + exp.toAst(); + }).toThrowError(); + }); + }); + + describe('#toString', () => { + test('generates an expression string from the AST', () => { + const exp = buildExpression(ast); + expect(exp.toString()).toMatchInlineSnapshot( + `"foo bar=\\"baz\\" subexp={hello world=false world=true}"` + ); + }); + + test('throws when called on an expression with no functions', () => { + ast.chain = []; + const exp = buildExpression(ast); + expect(() => { + exp.toString(); + }).toThrowError(); + }); + }); + + describe('#findFunction', () => { + test('finds a function by name', () => { + const exp = buildExpression(`where | is | waldo`); + const fns: ExpressionAstFunctionBuilder[] = exp.findFunction('waldo'); + expect(fns.map((fn) => fn.toAst())).toMatchInlineSnapshot(` + Array [ + Object { + "arguments": Object {}, + "function": "waldo", + "type": "function", + }, + ] + `); + }); + + test('recursively finds nested subexpressions', () => { + const exp = buildExpression( + `miss | miss sub={miss} | miss sub={hit sub={miss sub={hit sub={hit}}}} sub={miss}` + ); + const fns: ExpressionAstFunctionBuilder[] = exp.findFunction('hit'); + expect(fns.map((fn) => fn.name)).toMatchInlineSnapshot(` + Array [ + "hit", + "hit", + "hit", + ] + `); + }); + + test('retains references back to the original expression so you can perform migrations', () => { + const before = ` + foo sub={baz | bar a=1 sub={foo}} + | bar a=1 + | baz sub={bar a=1 c=4 sub={bar a=1 c=5}} + `; + + // Migrates all `bar` functions in the expression + const exp = buildExpression(before); + exp.findFunction('bar').forEach((fn) => { + const arg = fn.getArgument('a'); + if (arg) { + fn.replaceArgument('a', [1, 2]); + fn.addArgument('b', 3); + fn.removeArgument('c'); + } + }); + + expect(exp.toString()).toMatchInlineSnapshot(` + "foo sub={baz | bar a=1 a=2 sub={foo} b=3} + | bar a=1 a=2 b=3 + | baz sub={bar a=1 a=2 sub={bar a=1 a=2 b=3} b=3}" + `); + }); + + test('returns any subexpressions as expression builder instances', () => { + const exp = buildExpression( + `miss | miss sub={miss} | miss sub={hit sub={miss sub={hit sub={hit}}}} sub={miss}` + ); + const fns: ExpressionAstFunctionBuilder[] = exp.findFunction('hit'); + const subexpressionArgs = fns.map((fn) => + fn.getArgument('sub')?.map((arg) => isExpressionAstBuilder(arg)) + ); + expect(subexpressionArgs).toEqual([undefined, [true], [true]]); + }); + }); +}); diff --git a/src/plugins/expressions/common/ast/build_expression.ts b/src/plugins/expressions/common/ast/build_expression.ts new file mode 100644 index 0000000000000..b0a560600883a --- /dev/null +++ b/src/plugins/expressions/common/ast/build_expression.ts @@ -0,0 +1,169 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AnyExpressionFunctionDefinition } from '../expression_functions/types'; +import { ExpressionAstExpression, ExpressionAstFunction } from './types'; +import { + buildExpressionFunction, + ExpressionAstFunctionBuilder, + InferFunctionDefinition, +} from './build_function'; +import { format } from './format'; +import { parse } from './parse'; + +/** + * Type guard that checks whether a given value is an + * `ExpressionAstExpressionBuilder`. This is useful when working + * with subexpressions, where you might be retrieving a function + * argument, and need to know whether it is an expression builder + * instance which you can perform operations on. + * + * @example + * const arg = myFunction.getArgument('foo'); + * if (isExpressionAstBuilder(foo)) { + * foo.toAst(); + * } + * + * @param val Value you want to check. + * @return boolean + */ +export function isExpressionAstBuilder(val: any): val is ExpressionAstExpressionBuilder { + return val?.type === 'expression_builder'; +} + +/** @internal */ +export function isExpressionAst(val: any): val is ExpressionAstExpression { + return val?.type === 'expression'; +} + +export interface ExpressionAstExpressionBuilder { + /** + * Used to identify expression builder objects. + */ + type: 'expression_builder'; + /** + * Array of each of the `buildExpressionFunction()` instances + * in this expression. Use this to remove or reorder functions + * in the expression. + */ + functions: ExpressionAstFunctionBuilder[]; + /** + * Recursively searches expression for all ocurrences of the + * function, including in subexpressions. + * + * Useful when performing migrations on a specific function, + * as you can iterate over the array of references and update + * all functions at once. + * + * @param fnName Name of the function to search for. + * @return `ExpressionAstFunctionBuilder[]` + */ + findFunction: ( + fnName: InferFunctionDefinition['name'] + ) => Array> | []; + /** + * Converts expression to an AST. + * + * @return `ExpressionAstExpression` + */ + toAst: () => ExpressionAstExpression; + /** + * Converts expression to an expression string. + * + * @return `string` + */ + toString: () => string; +} + +const generateExpressionAst = (fns: ExpressionAstFunctionBuilder[]): ExpressionAstExpression => ({ + type: 'expression', + chain: fns.map((fn) => fn.toAst()), +}); + +/** + * Makes it easy to progressively build, update, and traverse an + * expression AST. You can either start with an empty AST, or + * provide an expression string, AST, or array of expression + * function builders to use as initial state. + * + * @param initialState Optional. An expression string, AST, or array of `ExpressionAstFunctionBuilder[]`. + * @return `this` + */ +export function buildExpression( + initialState?: ExpressionAstFunctionBuilder[] | ExpressionAstExpression | string +): ExpressionAstExpressionBuilder { + const chainToFunctionBuilder = (chain: ExpressionAstFunction[]): ExpressionAstFunctionBuilder[] => + chain.map((fn) => buildExpressionFunction(fn.function, fn.arguments)); + + // Takes `initialState` and converts it to an array of `ExpressionAstFunctionBuilder` + const extractFunctionsFromState = ( + state: ExpressionAstFunctionBuilder[] | ExpressionAstExpression | string + ): ExpressionAstFunctionBuilder[] => { + if (typeof state === 'string') { + return chainToFunctionBuilder(parse(state, 'expression').chain); + } else if (!Array.isArray(state)) { + // If it isn't an array, it is an `ExpressionAstExpression` + return chainToFunctionBuilder(state.chain); + } + return state; + }; + + const fns: ExpressionAstFunctionBuilder[] = initialState + ? extractFunctionsFromState(initialState) + : []; + + return { + type: 'expression_builder', + functions: fns, + + findFunction( + fnName: InferFunctionDefinition['name'] + ) { + const foundFns: Array> = []; + return fns.reduce((found, currFn) => { + Object.values(currFn.arguments).forEach((values) => { + values.forEach((value) => { + if (isExpressionAstBuilder(value)) { + // `value` is a subexpression, recurse and continue searching + found = found.concat(value.findFunction(fnName)); + } + }); + }); + if (currFn.name === fnName) { + found.push(currFn as ExpressionAstFunctionBuilder); + } + return found; + }, foundFns); + }, + + toAst() { + if (fns.length < 1) { + throw new Error('Functions have not been added to the expression builder'); + } + return generateExpressionAst(fns); + }, + + toString() { + if (fns.length < 1) { + throw new Error('Functions have not been added to the expression builder'); + } + return format(generateExpressionAst(fns), 'expression'); + }, + }; +} diff --git a/src/plugins/expressions/common/ast/build_function.test.ts b/src/plugins/expressions/common/ast/build_function.test.ts new file mode 100644 index 0000000000000..a2b54f31f6f8f --- /dev/null +++ b/src/plugins/expressions/common/ast/build_function.test.ts @@ -0,0 +1,399 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExpressionAstExpression } from './types'; +import { buildExpression } from './build_expression'; +import { buildExpressionFunction } from './build_function'; + +describe('buildExpressionFunction()', () => { + let subexp: ExpressionAstExpression; + let ast: ExpressionAstExpression; + + beforeEach(() => { + subexp = { + type: 'expression', + chain: [ + { + type: 'function', + function: 'hello', + arguments: { + world: [false, true], + }, + }, + ], + }; + ast = { + type: 'expression', + chain: [ + { + type: 'function', + function: 'foo', + arguments: { + bar: ['baz'], + subexp: [subexp], + }, + }, + ], + }; + }); + + test('accepts an args object as initial state', () => { + const fn = buildExpressionFunction('hello', { world: [true] }); + expect(fn.toAst()).toMatchInlineSnapshot(` + Object { + "arguments": Object { + "world": Array [ + true, + ], + }, + "function": "hello", + "type": "function", + } + `); + }); + + test('wraps any args in initial state in an array', () => { + const fn = buildExpressionFunction('hello', { world: true }); + expect(fn.arguments).toMatchInlineSnapshot(` + Object { + "world": Array [ + true, + ], + } + `); + }); + + test('returns all expected properties', () => { + const fn = buildExpressionFunction('hello', { world: [true] }); + expect(Object.keys(fn)).toMatchInlineSnapshot(` + Array [ + "type", + "name", + "arguments", + "addArgument", + "getArgument", + "replaceArgument", + "removeArgument", + "toAst", + "toString", + ] + `); + }); + + test('handles subexpressions in initial state', () => { + const fn = buildExpressionFunction(ast.chain[0].function, ast.chain[0].arguments); + expect(fn.toAst()).toMatchInlineSnapshot(` + Object { + "arguments": Object { + "bar": Array [ + "baz", + ], + "subexp": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "world": Array [ + false, + true, + ], + }, + "function": "hello", + "type": "function", + }, + ], + "type": "expression", + }, + ], + }, + "function": "foo", + "type": "function", + } + `); + }); + + test('handles subexpressions in multi-args in initial state', () => { + const subexpression = buildExpression([buildExpressionFunction('mySubexpression', {})]); + const fn = buildExpressionFunction('hello', { world: [true, subexpression] }); + expect(fn.toAst().arguments.world).toMatchInlineSnapshot(` + Array [ + true, + Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "mySubexpression", + "type": "function", + }, + ], + "type": "expression", + }, + ] + `); + }); + + describe('handles subexpressions as args', () => { + test('when provided an AST for the subexpression', () => { + const fn = buildExpressionFunction('hello', { world: [true] }); + fn.addArgument('subexp', buildExpression(subexp).toAst()); + expect(fn.toAst().arguments.subexp).toMatchInlineSnapshot(` + Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "world": Array [ + false, + true, + ], + }, + "function": "hello", + "type": "function", + }, + ], + "type": "expression", + }, + ] + `); + }); + + test('when provided a function builder for the subexpression', () => { + // test using `markdownVis`, which expects a subexpression + // using the `font` function + const anotherSubexpression = buildExpression([buildExpressionFunction('font', { size: 12 })]); + const fn = buildExpressionFunction('markdownVis', { + markdown: 'hello', + openLinksInNewTab: true, + font: anotherSubexpression, + }); + expect(fn.toAst().arguments.font).toMatchInlineSnapshot(` + Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "size": Array [ + 12, + ], + }, + "function": "font", + "type": "function", + }, + ], + "type": "expression", + }, + ] + `); + }); + + test('when subexpressions are changed by reference', () => { + const fontFn = buildExpressionFunction('font', { size: 12 }); + const fn = buildExpressionFunction('markdownVis', { + markdown: 'hello', + openLinksInNewTab: true, + font: buildExpression([fontFn]), + }); + fontFn.addArgument('color', 'blue'); + fontFn.replaceArgument('size', [72]); + expect(fn.toAst().arguments.font).toMatchInlineSnapshot(` + Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "color": Array [ + "blue", + ], + "size": Array [ + 72, + ], + }, + "function": "font", + "type": "function", + }, + ], + "type": "expression", + }, + ] + `); + }); + }); + + describe('#addArgument', () => { + test('allows you to add a new argument', () => { + const fn = buildExpressionFunction('hello', { world: [true] }); + fn.addArgument('world', false); + expect(fn.toAst().arguments).toMatchInlineSnapshot(` + Object { + "world": Array [ + true, + false, + ], + } + `); + }); + + test('creates new args if they do not yet exist', () => { + const fn = buildExpressionFunction('hello', { world: [true] }); + fn.addArgument('foo', 'bar'); + expect(fn.toAst().arguments).toMatchInlineSnapshot(` + Object { + "foo": Array [ + "bar", + ], + "world": Array [ + true, + ], + } + `); + }); + + test('mutates a function already associated with an expression', () => { + const fn = buildExpressionFunction('hello', { world: [true] }); + const exp = buildExpression([fn]); + fn.addArgument('foo', 'bar'); + expect(exp.toAst().chain).toMatchInlineSnapshot(` + Array [ + Object { + "arguments": Object { + "foo": Array [ + "bar", + ], + "world": Array [ + true, + ], + }, + "function": "hello", + "type": "function", + }, + ] + `); + fn.removeArgument('foo'); + expect(exp.toAst().chain).toMatchInlineSnapshot(` + Array [ + Object { + "arguments": Object { + "world": Array [ + true, + ], + }, + "function": "hello", + "type": "function", + }, + ] + `); + }); + }); + + describe('#getArgument', () => { + test('retrieves an arg by name', () => { + const fn = buildExpressionFunction('hello', { world: [true] }); + expect(fn.getArgument('world')).toEqual([true]); + }); + + test(`returns undefined when an arg doesn't exist`, () => { + const fn = buildExpressionFunction('hello', { world: [true] }); + expect(fn.getArgument('test')).toBe(undefined); + }); + + test('returned array can be updated to add/remove multiargs', () => { + const fn = buildExpressionFunction('hello', { world: [0, 1] }); + const arg = fn.getArgument('world'); + arg!.push(2); + expect(fn.getArgument('world')).toEqual([0, 1, 2]); + fn.replaceArgument( + 'world', + arg!.filter((a) => a !== 1) + ); + expect(fn.getArgument('world')).toEqual([0, 2]); + }); + }); + + describe('#toAst', () => { + test('returns a function AST', () => { + const fn = buildExpressionFunction('hello', { foo: [true] }); + expect(fn.toAst()).toMatchInlineSnapshot(` + Object { + "arguments": Object { + "foo": Array [ + true, + ], + }, + "function": "hello", + "type": "function", + } + `); + }); + }); + + describe('#toString', () => { + test('returns a function String', () => { + const fn = buildExpressionFunction('hello', { foo: [true], bar: ['hi'] }); + expect(fn.toString()).toMatchInlineSnapshot(`"hello foo=true bar=\\"hi\\""`); + }); + }); + + describe('#replaceArgument', () => { + test('allows you to replace an existing argument', () => { + const fn = buildExpressionFunction('hello', { world: [true] }); + fn.replaceArgument('world', [false]); + expect(fn.toAst().arguments).toMatchInlineSnapshot(` + Object { + "world": Array [ + false, + ], + } + `); + }); + + test('allows you to replace an existing argument with multi args', () => { + const fn = buildExpressionFunction('hello', { world: [true] }); + fn.replaceArgument('world', [true, false]); + expect(fn.toAst().arguments).toMatchInlineSnapshot(` + Object { + "world": Array [ + true, + false, + ], + } + `); + }); + + test('throws an error when replacing a non-existant arg', () => { + const fn = buildExpressionFunction('hello', { world: [true] }); + expect(() => { + fn.replaceArgument('whoops', [false]); + }).toThrowError(); + }); + }); + + describe('#removeArgument', () => { + test('removes an argument by name', () => { + const fn = buildExpressionFunction('hello', { foo: [true], bar: [false] }); + fn.removeArgument('bar'); + expect(fn.toAst().arguments).toMatchInlineSnapshot(` + Object { + "foo": Array [ + true, + ], + } + `); + }); + }); +}); diff --git a/src/plugins/expressions/common/ast/build_function.ts b/src/plugins/expressions/common/ast/build_function.ts new file mode 100644 index 0000000000000..5a1bd615d6450 --- /dev/null +++ b/src/plugins/expressions/common/ast/build_function.ts @@ -0,0 +1,243 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExpressionAstFunction } from './types'; +import { + AnyExpressionFunctionDefinition, + ExpressionFunctionDefinition, +} from '../expression_functions/types'; +import { + buildExpression, + ExpressionAstExpressionBuilder, + isExpressionAstBuilder, + isExpressionAst, +} from './build_expression'; +import { format } from './format'; + +// Infers the types from an ExpressionFunctionDefinition. +// @internal +export type InferFunctionDefinition< + FnDef extends AnyExpressionFunctionDefinition +> = FnDef extends ExpressionFunctionDefinition< + infer Name, + infer Input, + infer Arguments, + infer Output, + infer Context +> + ? { name: Name; input: Input; arguments: Arguments; output: Output; context: Context } + : never; + +// Shortcut for inferring args from a function definition. +type FunctionArgs = InferFunctionDefinition< + FnDef +>['arguments']; + +// Gets a list of possible arg names for a given function. +type FunctionArgName = { + [A in keyof FunctionArgs]: A extends string ? A : never; +}[keyof FunctionArgs]; + +// Gets all optional string keys from an interface. +type OptionalKeys = { + [K in keyof T]-?: {} extends Pick ? (K extends string ? K : never) : never; +}[keyof T]; + +// Represents the shape of arguments as they are stored +// in the function builder. +interface FunctionBuilderArguments { + [key: string]: Array[string] | ExpressionAstExpressionBuilder>; +} + +export interface ExpressionAstFunctionBuilder< + FnDef extends AnyExpressionFunctionDefinition = AnyExpressionFunctionDefinition +> { + /** + * Used to identify expression function builder objects. + */ + type: 'expression_function_builder'; + /** + * Name of this expression function. + */ + name: InferFunctionDefinition['name']; + /** + * Object of all args currently added to the function. This is + * structured similarly to `ExpressionAstFunction['arguments']`, + * however any subexpressions are returned as expression builder + * instances instead of expression ASTs. + */ + arguments: FunctionBuilderArguments; + /** + * Adds an additional argument to the function. For multi-args, + * this should be called once for each new arg. Note that TS + * will not enforce whether multi-args are available, so only + * use this to update an existing arg if you are certain it + * is a multi-arg. + * + * @param name The name of the argument to add. + * @param value The value of the argument to add. + * @return `this` + */ + addArgument:
>( + name: A, + value: FunctionArgs[A] | ExpressionAstExpressionBuilder + ) => this; + /** + * Retrieves an existing argument by name. + * Useful when you want to retrieve the current array of args and add + * something to it before calling `replaceArgument`. Any subexpression + * arguments will be returned as expression builder instances. + * + * @param name The name of the argument to retrieve. + * @return `ExpressionAstFunctionBuilderArgument[] | undefined` + */ + getArgument: >( + name: A + ) => Array[A] | ExpressionAstExpressionBuilder> | undefined; + /** + * Overwrites an existing argument with a new value. + * In order to support multi-args, the value given must always be + * an array. + * + * @param name The name of the argument to replace. + * @param value The value of the argument. Must always be an array. + * @return `this` + */ + replaceArgument: >( + name: A, + value: Array[A] | ExpressionAstExpressionBuilder> + ) => this; + /** + * Removes an (optional) argument from the function. + * + * TypeScript will enforce that you only remove optional + * arguments. For manipulating required args, use `replaceArgument`. + * + * @param name The name of the argument to remove. + * @return `this` + */ + removeArgument: >>(name: A) => this; + /** + * Converts function to an AST. + * + * @return `ExpressionAstFunction` + */ + toAst: () => ExpressionAstFunction; + /** + * Converts function to an expression string. + * + * @return `string` + */ + toString: () => string; +} + +/** + * Manages an AST for a single expression function. The return value + * can be provided to `buildExpression` to add this function to an + * expression. + * + * Note that to preserve type safety and ensure no args are missing, + * all required arguments for the specified function must be provided + * up front. If desired, they can be changed or removed later. + * + * @param fnName String representing the name of this expression function. + * @param initialArgs Object containing the arguments to this function. + * @return `this` + */ +export function buildExpressionFunction< + FnDef extends AnyExpressionFunctionDefinition = AnyExpressionFunctionDefinition +>( + fnName: InferFunctionDefinition['name'], + /** + * To support subexpressions, we override all args to also accept an + * ExpressionBuilder. This isn't perfectly typesafe since we don't + * know with certainty that the builder's output matches the required + * argument input, so we trust that folks using subexpressions in the + * builder know what they're doing. + */ + initialArgs: { + [K in keyof FunctionArgs]: + | FunctionArgs[K] + | ExpressionAstExpressionBuilder + | ExpressionAstExpressionBuilder[]; + } +): ExpressionAstFunctionBuilder { + const args = Object.entries(initialArgs).reduce((acc, [key, value]) => { + if (Array.isArray(value)) { + acc[key] = value.map((v) => { + return isExpressionAst(v) ? buildExpression(v) : v; + }); + } else { + acc[key] = isExpressionAst(value) ? [buildExpression(value)] : [value]; + } + return acc; + }, initialArgs as FunctionBuilderArguments); + + return { + type: 'expression_function_builder', + name: fnName, + arguments: args, + + addArgument(key, value) { + if (!args.hasOwnProperty(key)) { + args[key] = []; + } + args[key].push(value); + return this; + }, + + getArgument(key) { + if (!args.hasOwnProperty(key)) { + return; + } + return args[key]; + }, + + replaceArgument(key, values) { + if (!args.hasOwnProperty(key)) { + throw new Error('Argument to replace does not exist on this function'); + } + args[key] = values; + return this; + }, + + removeArgument(key) { + delete args[key]; + return this; + }, + + toAst() { + const ast: ExpressionAstFunction['arguments'] = {}; + return { + type: 'function', + function: fnName, + arguments: Object.entries(args).reduce((acc, [key, values]) => { + acc[key] = values.map((val) => { + return isExpressionAstBuilder(val) ? val.toAst() : val; + }); + return acc; + }, ast), + }; + }, + + toString() { + return format({ type: 'expression', chain: [this.toAst()] }, 'expression'); + }, + }; +} diff --git a/src/plugins/expressions/common/ast/format.test.ts b/src/plugins/expressions/common/ast/format.test.ts index d680ab2e30ce4..3d443c87b1ae2 100644 --- a/src/plugins/expressions/common/ast/format.test.ts +++ b/src/plugins/expressions/common/ast/format.test.ts @@ -17,11 +17,12 @@ * under the License. */ -import { formatExpression } from './format'; +import { ExpressionAstExpression, ExpressionAstArgument } from './types'; +import { format } from './format'; -describe('formatExpression()', () => { - test('converts expression AST to string', () => { - const str = formatExpression({ +describe('format()', () => { + test('formats an expression AST', () => { + const ast: ExpressionAstExpression = { type: 'expression', chain: [ { @@ -32,8 +33,13 @@ describe('formatExpression()', () => { function: 'foo', }, ], - }); + }; - expect(str).toMatchInlineSnapshot(`"foo bar=\\"baz\\""`); + expect(format(ast, 'expression')).toMatchInlineSnapshot(`"foo bar=\\"baz\\""`); + }); + + test('formats an argument', () => { + const ast: ExpressionAstArgument = 'foo'; + expect(format(ast, 'argument')).toMatchInlineSnapshot(`"\\"foo\\""`); }); }); diff --git a/src/plugins/expressions/common/ast/format.ts b/src/plugins/expressions/common/ast/format.ts index 985f07008b33d..7af0ab3350ab6 100644 --- a/src/plugins/expressions/common/ast/format.ts +++ b/src/plugins/expressions/common/ast/format.ts @@ -22,13 +22,9 @@ import { ExpressionAstExpression, ExpressionAstArgument } from './types'; // eslint-disable-next-line @typescript-eslint/no-var-requires const { toExpression } = require('@kbn/interpreter/common'); -export function format( - ast: ExpressionAstExpression | ExpressionAstArgument, - type: 'expression' | 'argument' +export function format( + ast: T, + type: T extends ExpressionAstExpression ? 'expression' : 'argument' ): string { return toExpression(ast, type); } - -export function formatExpression(ast: ExpressionAstExpression): string { - return format(ast, 'expression'); -} diff --git a/src/plugins/expressions/common/ast/format_expression.test.ts b/src/plugins/expressions/common/ast/format_expression.test.ts new file mode 100644 index 0000000000000..933fe78fc4dca --- /dev/null +++ b/src/plugins/expressions/common/ast/format_expression.test.ts @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { formatExpression } from './format_expression'; + +describe('formatExpression()', () => { + test('converts expression AST to string', () => { + const str = formatExpression({ + type: 'expression', + chain: [ + { + type: 'function', + arguments: { + bar: ['baz'], + }, + function: 'foo', + }, + ], + }); + + expect(str).toMatchInlineSnapshot(`"foo bar=\\"baz\\""`); + }); +}); diff --git a/src/plugins/expressions/common/ast/format_expression.ts b/src/plugins/expressions/common/ast/format_expression.ts new file mode 100644 index 0000000000000..cc9fe05fb85d2 --- /dev/null +++ b/src/plugins/expressions/common/ast/format_expression.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExpressionAstExpression } from './types'; +import { format } from './format'; + +/** + * Given expression pipeline AST, returns formatted string. + * + * @param ast Expression pipeline AST. + */ +export function formatExpression(ast: ExpressionAstExpression): string { + return format(ast, 'expression'); +} diff --git a/src/plugins/expressions/common/ast/index.ts b/src/plugins/expressions/common/ast/index.ts index 398718e8092b3..45ef8d45422eb 100644 --- a/src/plugins/expressions/common/ast/index.ts +++ b/src/plugins/expressions/common/ast/index.ts @@ -17,7 +17,10 @@ * under the License. */ -export * from './types'; -export * from './parse'; -export * from './parse_expression'; +export * from './build_expression'; +export * from './build_function'; +export * from './format_expression'; export * from './format'; +export * from './parse_expression'; +export * from './parse'; +export * from './types'; diff --git a/src/plugins/expressions/common/ast/parse.test.ts b/src/plugins/expressions/common/ast/parse.test.ts index 967091a52082f..77487f0a1ee90 100644 --- a/src/plugins/expressions/common/ast/parse.test.ts +++ b/src/plugins/expressions/common/ast/parse.test.ts @@ -37,6 +37,12 @@ describe('parse()', () => { }); }); + test('throws on malformed expression', () => { + expect(() => { + parse('{ intentionally malformed }', 'expression'); + }).toThrowError(); + }); + test('parses an argument', () => { const arg = parse('foo', 'argument'); expect(arg).toBe('foo'); diff --git a/src/plugins/expressions/common/ast/parse.ts b/src/plugins/expressions/common/ast/parse.ts index 0204694d1926d..f02c51d7b6799 100644 --- a/src/plugins/expressions/common/ast/parse.ts +++ b/src/plugins/expressions/common/ast/parse.ts @@ -22,10 +22,10 @@ import { ExpressionAstExpression, ExpressionAstArgument } from './types'; // eslint-disable-next-line @typescript-eslint/no-var-requires const { parse: parseRaw } = require('@kbn/interpreter/common'); -export function parse( - expression: string, - startRule: 'expression' | 'argument' -): ExpressionAstExpression | ExpressionAstArgument { +export function parse( + expression: E, + startRule: S +): S extends 'expression' ? ExpressionAstExpression : ExpressionAstArgument { try { return parseRaw(String(expression), { startRule }); } catch (e) { diff --git a/src/plugins/expressions/common/ast/parse_expression.ts b/src/plugins/expressions/common/ast/parse_expression.ts index ae4d80bd1fb5b..1ae542aa3d0c7 100644 --- a/src/plugins/expressions/common/ast/parse_expression.ts +++ b/src/plugins/expressions/common/ast/parse_expression.ts @@ -26,5 +26,5 @@ import { parse } from './parse'; * @param expression Expression pipeline string. */ export function parseExpression(expression: string): ExpressionAstExpression { - return parse(expression, 'expression') as ExpressionAstExpression; + return parse(expression, 'expression'); } diff --git a/src/plugins/expressions/common/expression_functions/specs/clog.ts b/src/plugins/expressions/common/expression_functions/specs/clog.ts index 7839f1fc7998d..28294af04c881 100644 --- a/src/plugins/expressions/common/expression_functions/specs/clog.ts +++ b/src/plugins/expressions/common/expression_functions/specs/clog.ts @@ -19,7 +19,9 @@ import { ExpressionFunctionDefinition } from '../types'; -export const clog: ExpressionFunctionDefinition<'clog', unknown, {}, unknown> = { +export type ExpressionFunctionClog = ExpressionFunctionDefinition<'clog', unknown, {}, unknown>; + +export const clog: ExpressionFunctionClog = { name: 'clog', args: {}, help: 'Outputs the context to the console', diff --git a/src/plugins/expressions/common/expression_functions/specs/font.ts b/src/plugins/expressions/common/expression_functions/specs/font.ts index c8016bfacc710..c46ce0adadef0 100644 --- a/src/plugins/expressions/common/expression_functions/specs/font.ts +++ b/src/plugins/expressions/common/expression_functions/specs/font.ts @@ -52,7 +52,9 @@ interface Arguments { weight?: FontWeight; } -export const font: ExpressionFunctionDefinition<'font', null, Arguments, Style> = { +export type ExpressionFunctionFont = ExpressionFunctionDefinition<'font', null, Arguments, Style>; + +export const font: ExpressionFunctionFont = { name: 'font', aliases: [], type: 'style', diff --git a/src/plugins/expressions/common/expression_functions/specs/var.ts b/src/plugins/expressions/common/expression_functions/specs/var.ts index e90a21101c557..4bc185a4cadfd 100644 --- a/src/plugins/expressions/common/expression_functions/specs/var.ts +++ b/src/plugins/expressions/common/expression_functions/specs/var.ts @@ -24,7 +24,12 @@ interface Arguments { name: string; } -type ExpressionFunctionVar = ExpressionFunctionDefinition<'var', unknown, Arguments, unknown>; +export type ExpressionFunctionVar = ExpressionFunctionDefinition< + 'var', + unknown, + Arguments, + unknown +>; export const variable: ExpressionFunctionVar = { name: 'var', diff --git a/src/plugins/expressions/common/expression_functions/specs/var_set.ts b/src/plugins/expressions/common/expression_functions/specs/var_set.ts index 0bf89f5470b3d..8f15bc8b90042 100644 --- a/src/plugins/expressions/common/expression_functions/specs/var_set.ts +++ b/src/plugins/expressions/common/expression_functions/specs/var_set.ts @@ -25,7 +25,14 @@ interface Arguments { value?: any; } -export const variableSet: ExpressionFunctionDefinition<'var_set', unknown, Arguments, unknown> = { +export type ExpressionFunctionVarSet = ExpressionFunctionDefinition< + 'var_set', + unknown, + Arguments, + unknown +>; + +export const variableSet: ExpressionFunctionVarSet = { name: 'var_set', help: i18n.translate('expressions.functions.varset.help', { defaultMessage: 'Updates kibana global context', diff --git a/src/plugins/expressions/common/expression_functions/types.ts b/src/plugins/expressions/common/expression_functions/types.ts index b91deea36aee8..5979bcffb3175 100644 --- a/src/plugins/expressions/common/expression_functions/types.ts +++ b/src/plugins/expressions/common/expression_functions/types.ts @@ -21,6 +21,14 @@ import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; import { ArgumentType } from './arguments'; import { TypeToString } from '../types/common'; import { ExecutionContext } from '../execution/types'; +import { + ExpressionFunctionClog, + ExpressionFunctionFont, + ExpressionFunctionKibanaContext, + ExpressionFunctionKibana, + ExpressionFunctionVarSet, + ExpressionFunctionVar, +} from './specs'; /** * `ExpressionFunctionDefinition` is the interface plugins have to implement to @@ -29,7 +37,7 @@ import { ExecutionContext } from '../execution/types'; export interface ExpressionFunctionDefinition< Name extends string, Input, - Arguments, + Arguments extends Record, Output, Context extends ExecutionContext = ExecutionContext > { @@ -93,4 +101,25 @@ export interface ExpressionFunctionDefinition< /** * Type to capture every possible expression function definition. */ -export type AnyExpressionFunctionDefinition = ExpressionFunctionDefinition; +export type AnyExpressionFunctionDefinition = ExpressionFunctionDefinition< + string, + any, + Record, + any +>; + +/** + * A mapping of `ExpressionFunctionDefinition`s for functions which the + * Expressions services provides out-of-the-box. Any new functions registered + * by the Expressions plugin should have their types added here. + * + * @public + */ +export interface ExpressionFunctionDefinitions { + clog: ExpressionFunctionClog; + font: ExpressionFunctionFont; + kibana_context: ExpressionFunctionKibanaContext; + kibana: ExpressionFunctionKibana; + var_set: ExpressionFunctionVarSet; + var: ExpressionFunctionVar; +} diff --git a/src/plugins/expressions/public/index.ts b/src/plugins/expressions/public/index.ts index 336a80d98a110..87406db89a2a8 100644 --- a/src/plugins/expressions/public/index.ts +++ b/src/plugins/expressions/public/index.ts @@ -42,6 +42,8 @@ export { AnyExpressionFunctionDefinition, AnyExpressionTypeDefinition, ArgumentType, + buildExpression, + buildExpressionFunction, Datatable, DatatableColumn, DatatableColumnType, @@ -57,10 +59,13 @@ export { ExecutorState, ExpressionAstArgument, ExpressionAstExpression, + ExpressionAstExpressionBuilder, ExpressionAstFunction, + ExpressionAstFunctionBuilder, ExpressionAstNode, ExpressionFunction, ExpressionFunctionDefinition, + ExpressionFunctionDefinitions, ExpressionFunctionKibana, ExpressionFunctionParameter, ExpressionImage, @@ -90,6 +95,7 @@ export { IInterpreterRenderHandlers, InterpreterErrorType, IRegistry, + isExpressionAstBuilder, KIBANA_CONTEXT_NAME, KibanaContext, KibanaDatatable, diff --git a/src/plugins/expressions/server/index.ts b/src/plugins/expressions/server/index.ts index 61d3838466bef..9b2f0b794258b 100644 --- a/src/plugins/expressions/server/index.ts +++ b/src/plugins/expressions/server/index.ts @@ -34,6 +34,8 @@ export { AnyExpressionFunctionDefinition, AnyExpressionTypeDefinition, ArgumentType, + buildExpression, + buildExpressionFunction, Datatable, DatatableColumn, DatatableColumnType, @@ -48,10 +50,13 @@ export { ExecutorState, ExpressionAstArgument, ExpressionAstExpression, + ExpressionAstExpressionBuilder, ExpressionAstFunction, + ExpressionAstFunctionBuilder, ExpressionAstNode, ExpressionFunction, ExpressionFunctionDefinition, + ExpressionFunctionDefinitions, ExpressionFunctionKibana, ExpressionFunctionParameter, ExpressionImage, @@ -81,6 +86,7 @@ export { IInterpreterRenderHandlers, InterpreterErrorType, IRegistry, + isExpressionAstBuilder, KIBANA_CONTEXT_NAME, KibanaContext, KibanaDatatable, From a5c9c4ec4324f7432dbe083ba7eb1c2a63896a45 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Wed, 17 Jun 2020 16:24:40 -0400 Subject: [PATCH 50/82] [CI] Add baseline trigger job --- .ci/Jenkinsfile_baseline_trigger | 64 ++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .ci/Jenkinsfile_baseline_trigger diff --git a/.ci/Jenkinsfile_baseline_trigger b/.ci/Jenkinsfile_baseline_trigger new file mode 100644 index 0000000000000..05daeebdc058c --- /dev/null +++ b/.ci/Jenkinsfile_baseline_trigger @@ -0,0 +1,64 @@ +#!/bin/groovy + +def MAXIMUM_COMMITS_TO_CHECK = 10 +def MAXIMUM_COMMITS_TO_BUILD = 5 + +if (!params.branches_yaml) { + error "'branches_yaml' parameter must be specified" +} + +def additionalBranches = [] + +def branches = readYaml(text: params.branches_yaml) + additionalBranches + +library 'kibana-pipeline-library' +kibanaLibrary.load() + +withGithubCredentials { + branches.each { branch -> + stage(branch) { + def commits = getCommits(branch, MAXIMUM_COMMITS_TO_CHECK, MAXIMUM_COMMITS_TO_BUILD) + + commits.take(MAXIMUM_COMMITS_TO_BUILD).each { commit -> + catchErrors { + githubCommitStatus.create(commit, 'pending', 'Baseline started.', context = 'kibana-ci-baseline') + + build( + propagate: false, + wait: false, + job: 'elastic+kibana+baseline', + parameters: [ + string(name: 'branch_specifier', value: branch), + string(name: 'commit', value: commit), + ] + ) + } + } + } + } +} + +def getCommits(String branch, maximumCommitsToCheck, maximumCommitsToBuild) { + print "Getting latest commits for ${branch}..." + def commits = githubApi.get("repos/elastic/kibana/commits?sha=${branch}").take(maximumCommitsToCheck).collect { it.sha } + def commitsToBuild = [] + + for (commit in commits) { + print "Getting statuses for ${commit}" + def status = githubApi.get("repos/elastic/kibana/statuses/${commit}").find { it.context == 'kibana-ci-baseline' } + print "Commit '${commit}' already built? ${status ? 'Yes' : 'No'}" + + if (!status) { + commitsToBuild << commit + } else { + // Stop at the first commit we find that's already been triggered + break + } + + if (commitsToBuild.size() >= maximumCommitsToBuild) { + break + } + } + + return commitsToBuild.reverse() // We want the builds to trigger oldest-to-newest +} From a81d8b55ab2d941010137e4019c015ff77687721 Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 13 Jul 2020 16:15:48 -0700 Subject: [PATCH 51/82] rename visual_baseline -> baseline_capture --- ..._visual_baseline => Jenkinsfile_baseline_capture} | 0 test/scripts/jenkins_xpack_visual_regression.sh | 12 ++++++------ 2 files changed, 6 insertions(+), 6 deletions(-) rename .ci/{Jenkinsfile_visual_baseline => Jenkinsfile_baseline_capture} (100%) diff --git a/.ci/Jenkinsfile_visual_baseline b/.ci/Jenkinsfile_baseline_capture similarity index 100% rename from .ci/Jenkinsfile_visual_baseline rename to .ci/Jenkinsfile_baseline_capture diff --git a/test/scripts/jenkins_xpack_visual_regression.sh b/test/scripts/jenkins_xpack_visual_regression.sh index ac567a188a6d4..06a53277b8688 100755 --- a/test/scripts/jenkins_xpack_visual_regression.sh +++ b/test/scripts/jenkins_xpack_visual_regression.sh @@ -11,6 +11,12 @@ installDir="$PARENT_DIR/install/kibana" mkdir -p "$installDir" tar -xzf "$linuxBuild" -C "$installDir" --strip=1 +# cd "$KIBANA_DIR" +# source "test/scripts/jenkins_xpack_page_load_metrics.sh" + +cd "$KIBANA_DIR" +source "test/scripts/jenkins_xpack_saved_objects_field_metrics.sh" + echo " -> running visual regression tests from x-pack directory" cd "$XPACK_DIR" yarn percy exec -t 10000 -- -- \ @@ -18,9 +24,3 @@ yarn percy exec -t 10000 -- -- \ --debug --bail \ --kibana-install-dir "$installDir" \ --config test/visual_regression/config.ts; - -# cd "$KIBANA_DIR" -# source "test/scripts/jenkins_xpack_page_load_metrics.sh" - -cd "$KIBANA_DIR" -source "test/scripts/jenkins_xpack_saved_objects_field_metrics.sh" From 0e7c3c7ff09e2e1daa4b1eba93c62059eb5fe3c1 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 14 Jul 2020 16:07:22 -0600 Subject: [PATCH 52/82] [Maps] increase DEFAULT_MAX_BUCKETS_LIMIT to 65535 (#70313) Co-authored-by: Elastic Machine --- x-pack/plugins/maps/common/constants.ts | 2 +- .../maps/public/classes/fields/es_agg_field.ts | 6 ++++-- .../sources/es_geo_grid_source/es_geo_grid_source.js | 3 +++ .../plugins/maps/public/elasticsearch_geo_utils.js | 5 +++-- .../maps/public/elasticsearch_geo_utils.test.js | 12 ++++++------ 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 98464427cc348..cf67ac4dd999f 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -90,7 +90,7 @@ export const DECIMAL_DEGREES_PRECISION = 5; // meters precision export const ZOOM_PRECISION = 2; export const DEFAULT_MAX_RESULT_WINDOW = 10000; export const DEFAULT_MAX_INNER_RESULT_WINDOW = 100; -export const DEFAULT_MAX_BUCKETS_LIMIT = 10000; +export const DEFAULT_MAX_BUCKETS_LIMIT = 65535; export const FEATURE_ID_PROPERTY_NAME = '__kbn__feature_id__'; export const FEATURE_VISIBLE_PROPERTY_NAME = '__kbn_isvisibleduetojoin__'; diff --git a/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts b/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts index e0f5c79f1d427..15779d22681c0 100644 --- a/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts @@ -17,6 +17,8 @@ import { TopTermPercentageField } from './top_term_percentage_field'; import { ITooltipProperty, TooltipProperty } from '../tooltips/tooltip_property'; import { ESAggTooltipProperty } from '../tooltips/es_agg_tooltip_property'; +const TERMS_AGG_SHARD_SIZE = 5; + export interface IESAggField extends IField { getValueAggDsl(indexPattern: IndexPattern): unknown | null; getBucketCount(): number; @@ -100,7 +102,7 @@ export class ESAggField implements IESAggField { const field = getField(indexPattern, this.getRootName()); const aggType = this.getAggType(); - const aggBody = aggType === AGG_TYPE.TERMS ? { size: 1, shard_size: 1 } : {}; + const aggBody = aggType === AGG_TYPE.TERMS ? { size: 1, shard_size: TERMS_AGG_SHARD_SIZE } : {}; return { [aggType]: addFieldToDSL(aggBody, field), }; @@ -108,7 +110,7 @@ export class ESAggField implements IESAggField { getBucketCount(): number { // terms aggregation increases the overall number of buckets per split bucket - return this.getAggType() === AGG_TYPE.TERMS ? 1 : 0; + return this.getAggType() === AGG_TYPE.TERMS ? TERMS_AGG_SHARD_SIZE : 0; } supportsFieldMeta(): boolean { diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js index 3902709eeb841..92f6c258af597 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js @@ -161,6 +161,7 @@ export class ESGeoGridSource extends AbstractESAggSource { bounds: makeESBbox(bufferedExtent), field: this._descriptor.geoField, precision, + size: DEFAULT_MAX_BUCKETS_LIMIT, }, }, }, @@ -245,6 +246,8 @@ export class ESGeoGridSource extends AbstractESAggSource { bounds: makeESBbox(bufferedExtent), field: this._descriptor.geoField, precision, + size: DEFAULT_MAX_BUCKETS_LIMIT, + shard_size: DEFAULT_MAX_BUCKETS_LIMIT, }, aggs: { gridCentroid: { diff --git a/x-pack/plugins/maps/public/elasticsearch_geo_utils.js b/x-pack/plugins/maps/public/elasticsearch_geo_utils.js index efd243595db3e..0d247d389f478 100644 --- a/x-pack/plugins/maps/public/elasticsearch_geo_utils.js +++ b/x-pack/plugins/maps/public/elasticsearch_geo_utils.js @@ -400,8 +400,9 @@ export function getBoundingBoxGeometry(geometry) { export function formatEnvelopeAsPolygon({ maxLat, maxLon, minLat, minLon }) { // GeoJSON mandates that the outer polygon must be counterclockwise to avoid ambiguous polygons // when the shape crosses the dateline - const left = minLon; - const right = maxLon; + const lonDelta = maxLon - minLon; + const left = lonDelta > 360 ? -180 : minLon; + const right = lonDelta > 360 ? 180 : maxLon; const top = clampToLatBounds(maxLat); const bottom = clampToLatBounds(minLat); const topLeft = [left, top]; diff --git a/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js b/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js index a1e4e43f3ab75..adaeae66bee14 100644 --- a/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js +++ b/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js @@ -421,7 +421,7 @@ describe('createExtentFilter', () => { }); }); - it('should not clamp longitudes to -180 to 180', () => { + it('should clamp longitudes to -180 to 180 when lonitude wraps globe', () => { const mapExtent = { maxLat: 39, maxLon: 209, @@ -436,11 +436,11 @@ describe('createExtentFilter', () => { shape: { coordinates: [ [ - [-191, 39], - [-191, 35], - [209, 35], - [209, 39], - [-191, 39], + [-180, 39], + [-180, 35], + [180, 35], + [180, 39], + [-180, 39], ], ], type: 'Polygon', From e42630d1c58c2587e34959c8037e4ac6b9d27472 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Tue, 14 Jul 2020 18:08:20 -0400 Subject: [PATCH 53/82] [Security Solution] [DETECTIONS] Set rule status to failure only on large gaps (#71549) * only display gap error when a gap is too large for the gap mitigation code to cover, general code cleanup, adds some tests for separate function * removes throwing of errors and log error and return null for maxCatchup, ratio, and gapDiffInUnits properties * forgot to delete commented out code * remove math.abs since we fixed this bug by switching around logic when calculating gapDiffInUnits in getGapMaxCatchupRatio fn * updates tests for when a gap error should be written to rule status * fix typo --- .../signals/signal_rule_alert_type.test.ts | 36 ++- .../signals/signal_rule_alert_type.ts | 36 ++- .../lib/detection_engine/signals/types.ts | 5 + .../detection_engine/signals/utils.test.ts | 47 ++++ .../lib/detection_engine/signals/utils.ts | 218 ++++++++++++------ 5 files changed, 258 insertions(+), 84 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 5832b4075a40b..b0c855afa8be9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -10,7 +10,13 @@ import { getResult, getMlResult } from '../routes/__mocks__/request_responses'; import { signalRulesAlertType } from './signal_rule_alert_type'; import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; import { ruleStatusServiceFactory } from './rule_status_service'; -import { getGapBetweenRuns, getListsClient, getExceptions, sortExceptionItems } from './utils'; +import { + getGapBetweenRuns, + getGapMaxCatchupRatio, + getListsClient, + getExceptions, + sortExceptionItems, +} from './utils'; import { RuleExecutorOptions } from './types'; import { searchAfterAndBulkCreate } from './search_after_bulk_create'; import { scheduleNotificationActions } from '../notifications/schedule_notification_actions'; @@ -97,6 +103,7 @@ describe('rules_notification_alert_type', () => { exceptionsWithValueLists: [], }); (searchAfterAndBulkCreate as jest.Mock).mockClear(); + (getGapMaxCatchupRatio as jest.Mock).mockClear(); (searchAfterAndBulkCreate as jest.Mock).mockResolvedValue({ success: true, searchAfterTimes: [], @@ -126,22 +133,39 @@ describe('rules_notification_alert_type', () => { }); describe('executor', () => { - it('should warn about the gap between runs', async () => { - (getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(1000)); + it('should warn about the gap between runs if gap is very large', async () => { + (getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(100, 'm')); + (getGapMaxCatchupRatio as jest.Mock).mockReturnValue({ + maxCatchup: 4, + ratio: 20, + gapDiffInUnits: 95, + }); await alert.executor(payload); expect(logger.warn).toHaveBeenCalled(); expect(logger.warn.mock.calls[0][0]).toContain( - 'a few seconds (1000ms) has passed since last rule execution, and signals may have been missed.' + '2 hours (6000000ms) has passed since last rule execution, and signals may have been missed.' ); expect(ruleStatusService.error).toHaveBeenCalled(); expect(ruleStatusService.error.mock.calls[0][0]).toContain( - 'a few seconds (1000ms) has passed since last rule execution, and signals may have been missed.' + '2 hours (6000000ms) has passed since last rule execution, and signals may have been missed.' ); expect(ruleStatusService.error.mock.calls[0][1]).toEqual({ - gap: 'a few seconds', + gap: '2 hours', }); }); + it('should NOT warn about the gap between runs if gap small', async () => { + (getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(1, 'm')); + (getGapMaxCatchupRatio as jest.Mock).mockReturnValue({ + maxCatchup: 1, + ratio: 1, + gapDiffInUnits: 1, + }); + await alert.executor(payload); + expect(logger.warn).toHaveBeenCalledTimes(0); + expect(ruleStatusService.error).toHaveBeenCalledTimes(0); + }); + it("should set refresh to 'wait_for' when actions are present", async () => { const ruleAlert = getResult(); ruleAlert.actions = [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 49efc30b9704d..0e859ecef31c6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -22,7 +22,14 @@ import { } from './search_after_bulk_create'; import { getFilter } from './get_filter'; import { SignalRuleAlertTypeDefinition, RuleAlertAttributes } from './types'; -import { getGapBetweenRuns, parseScheduleDates, getListsClient, getExceptions } from './utils'; +import { + getGapBetweenRuns, + parseScheduleDates, + getListsClient, + getExceptions, + getGapMaxCatchupRatio, + MAX_RULE_GAP_RATIO, +} from './utils'; import { signalParamsSchema } from './signal_params_schema'; import { siemRuleActionGroups } from './siem_rule_action_groups'; import { findMlSignals } from './find_ml_signals'; @@ -130,15 +137,26 @@ export const signalRulesAlertType = ({ const gap = getGapBetweenRuns({ previousStartedAt, interval, from, to }); if (gap != null && gap.asMilliseconds() > 0) { - const gapString = gap.humanize(); - const gapMessage = buildRuleMessage( - `${gapString} (${gap.asMilliseconds()}ms) has passed since last rule execution, and signals may have been missed.`, - 'Consider increasing your look behind time or adding more Kibana instances.' - ); - logger.warn(gapMessage); + const fromUnit = from[from.length - 1]; + const { ratio } = getGapMaxCatchupRatio({ + logger, + buildRuleMessage, + previousStartedAt, + ruleParamsFrom: from, + interval, + unit: fromUnit, + }); + if (ratio && ratio >= MAX_RULE_GAP_RATIO) { + const gapString = gap.humanize(); + const gapMessage = buildRuleMessage( + `${gapString} (${gap.asMilliseconds()}ms) has passed since last rule execution, and signals may have been missed.`, + 'Consider increasing your look behind time or adding more Kibana instances.' + ); + logger.warn(gapMessage); - hasError = true; - await ruleStatusService.error(gapMessage, { gap: gapString }); + hasError = true; + await ruleStatusService.error(gapMessage, { gap: gapString }); + } } try { const { listClient, exceptionsClient } = await getListsClient({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 5d6bafc5a6d09..bfc72a169566e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -11,6 +11,11 @@ import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams } from '../types'; import { SearchResponse } from '../../types'; +// used for gap detection code +export type unitType = 's' | 'm' | 'h'; +export const isValidUnit = (unitParam: string): unitParam is unitType => + ['s', 'm', 'h'].includes(unitParam); + export interface SignalsParams { signalIds: string[] | undefined | null; query: object | undefined | null; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index 0cc3ca092a4dc..a6130a20f9c52 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -21,6 +21,7 @@ import { parseScheduleDates, getDriftTolerance, getGapBetweenRuns, + getGapMaxCatchupRatio, errorAggregator, getListsClient, hasLargeValueList, @@ -716,6 +717,52 @@ describe('utils', () => { }); }); + describe('getMaxCatchupRatio', () => { + test('should return null if rule has never run before', () => { + const { maxCatchup, ratio, gapDiffInUnits } = getGapMaxCatchupRatio({ + logger: mockLogger, + previousStartedAt: null, + interval: '30s', + ruleParamsFrom: 'now-30s', + buildRuleMessage, + unit: 's', + }); + expect(maxCatchup).toBeNull(); + expect(ratio).toBeNull(); + expect(gapDiffInUnits).toBeNull(); + }); + + test('should should have non-null values when gap is present', () => { + const { maxCatchup, ratio, gapDiffInUnits } = getGapMaxCatchupRatio({ + logger: mockLogger, + previousStartedAt: moment().subtract(65, 's').toDate(), + interval: '50s', + ruleParamsFrom: 'now-55s', + buildRuleMessage, + unit: 's', + }); + expect(maxCatchup).toEqual(0.2); + expect(ratio).toEqual(0.2); + expect(gapDiffInUnits).toEqual(10); + }); + + // when a rule runs sooner than expected we don't + // consider that a gap as that is a very rare circumstance + test('should return null when given a negative gap (rule ran sooner than expected)', () => { + const { maxCatchup, ratio, gapDiffInUnits } = getGapMaxCatchupRatio({ + logger: mockLogger, + previousStartedAt: moment().subtract(-15, 's').toDate(), + interval: '10s', + ruleParamsFrom: 'now-13s', + buildRuleMessage, + unit: 's', + }); + expect(maxCatchup).toBeNull(); + expect(ratio).toBeNull(); + expect(gapDiffInUnits).toBeNull(); + }); + }); + describe('#getExceptions', () => { test('it successfully returns array of exception list items', async () => { const client = listMock.getExceptionListClient(); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 0016765b9dbe9..0b95ff6786b01 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -12,7 +12,7 @@ import { AlertServices, parseDuration } from '../../../../../alerts/server'; import { ExceptionListClient, ListClient, ListPluginSetup } from '../../../../../lists/server'; import { EntriesArray, ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { ListArrayOrUndefined } from '../../../../common/detection_engine/schemas/types/lists'; -import { BulkResponse, BulkResponseErrorAggregation } from './types'; +import { BulkResponse, BulkResponseErrorAggregation, isValidUnit } from './types'; import { BuildRuleMessage } from './rule_messages'; interface SortExceptionsReturn { @@ -20,6 +20,101 @@ interface SortExceptionsReturn { exceptionsWithoutValueLists: ExceptionListItemSchema[]; } +export const MAX_RULE_GAP_RATIO = 4; + +export const shorthandMap = { + s: { + momentString: 'seconds', + asFn: (duration: moment.Duration) => duration.asSeconds(), + }, + m: { + momentString: 'minutes', + asFn: (duration: moment.Duration) => duration.asMinutes(), + }, + h: { + momentString: 'hours', + asFn: (duration: moment.Duration) => duration.asHours(), + }, +}; + +export const getGapMaxCatchupRatio = ({ + logger, + previousStartedAt, + unit, + buildRuleMessage, + ruleParamsFrom, + interval, +}: { + logger: Logger; + ruleParamsFrom: string; + previousStartedAt: Date | null | undefined; + interval: string; + buildRuleMessage: BuildRuleMessage; + unit: string; +}): { + maxCatchup: number | null; + ratio: number | null; + gapDiffInUnits: number | null; +} => { + if (previousStartedAt == null) { + return { + maxCatchup: null, + ratio: null, + gapDiffInUnits: null, + }; + } + if (!isValidUnit(unit)) { + logger.error(buildRuleMessage(`unit: ${unit} failed isValidUnit check`)); + return { + maxCatchup: null, + ratio: null, + gapDiffInUnits: null, + }; + } + /* + we need the total duration from now until the last time the rule ran. + the next few lines can be summed up as calculating + "how many second | minutes | hours have passed since the last time this ran?" + */ + const nowToGapDiff = moment.duration(moment().diff(previousStartedAt)); + // rule ran early, no gap + if (shorthandMap[unit].asFn(nowToGapDiff) < 0) { + // rule ran early, no gap + return { + maxCatchup: null, + ratio: null, + gapDiffInUnits: null, + }; + } + const calculatedFrom = `now-${ + parseInt(shorthandMap[unit].asFn(nowToGapDiff).toString(), 10) + unit + }`; + logger.debug(buildRuleMessage(`calculatedFrom: ${calculatedFrom}`)); + + const intervalMoment = moment.duration(parseInt(interval, 10), unit); + logger.debug(buildRuleMessage(`intervalMoment: ${shorthandMap[unit].asFn(intervalMoment)}`)); + const calculatedFromAsMoment = dateMath.parse(calculatedFrom); + const dateMathRuleParamsFrom = dateMath.parse(ruleParamsFrom); + if (dateMathRuleParamsFrom != null && intervalMoment != null) { + const momentUnit = shorthandMap[unit].momentString as moment.DurationInputArg2; + const gapDiffInUnits = dateMathRuleParamsFrom.diff(calculatedFromAsMoment, momentUnit); + + const ratio = gapDiffInUnits / shorthandMap[unit].asFn(intervalMoment); + + // maxCatchup is to ensure we are not trying to catch up too far back. + // This allows for a maximum of 4 consecutive rule execution misses + // to be included in the number of signals generated. + const maxCatchup = ratio < MAX_RULE_GAP_RATIO ? ratio : MAX_RULE_GAP_RATIO; + return { maxCatchup, ratio, gapDiffInUnits }; + } + logger.error(buildRuleMessage('failed to parse calculatedFrom and intervalMoment')); + return { + maxCatchup: null, + ratio: null, + gapDiffInUnits: null, + }; +}; + export const getListsClient = async ({ lists, spaceId, @@ -294,8 +389,6 @@ export const getSignalTimeTuples = ({ from: moment.Moment | undefined; maxSignals: number; }> => { - type unitType = 's' | 'm' | 'h'; - const isValidUnit = (unit: string): unit is unitType => ['s', 'm', 'h'].includes(unit); let totalToFromTuples: Array<{ to: moment.Moment | undefined; from: moment.Moment | undefined; @@ -305,20 +398,6 @@ export const getSignalTimeTuples = ({ const fromUnit = ruleParamsFrom[ruleParamsFrom.length - 1]; if (isValidUnit(fromUnit)) { const unit = fromUnit; // only seconds (s), minutes (m) or hours (h) - const shorthandMap = { - s: { - momentString: 'seconds', - asFn: (duration: moment.Duration) => duration.asSeconds(), - }, - m: { - momentString: 'minutes', - asFn: (duration: moment.Duration) => duration.asMinutes(), - }, - h: { - momentString: 'hours', - asFn: (duration: moment.Duration) => duration.asHours(), - }, - }; /* we need the total duration from now until the last time the rule ran. @@ -333,62 +412,63 @@ export const getSignalTimeTuples = ({ const intervalMoment = moment.duration(parseInt(interval, 10), unit); logger.debug(buildRuleMessage(`intervalMoment: ${shorthandMap[unit].asFn(intervalMoment)}`)); - const calculatedFromAsMoment = dateMath.parse(calculatedFrom); - if (calculatedFromAsMoment != null && intervalMoment != null) { - const dateMathRuleParamsFrom = dateMath.parse(ruleParamsFrom); - const momentUnit = shorthandMap[unit].momentString as moment.DurationInputArg2; - const gapDiffInUnits = calculatedFromAsMoment.diff(dateMathRuleParamsFrom, momentUnit); - - const ratio = Math.abs(gapDiffInUnits / shorthandMap[unit].asFn(intervalMoment)); - - // maxCatchup is to ensure we are not trying to catch up too far back. - // This allows for a maximum of 4 consecutive rule execution misses - // to be included in the number of signals generated. - const maxCatchup = ratio < 4 ? ratio : 4; - logger.debug(buildRuleMessage(`maxCatchup: ${ratio}`)); + const momentUnit = shorthandMap[unit].momentString as moment.DurationInputArg2; + // maxCatchup is to ensure we are not trying to catch up too far back. + // This allows for a maximum of 4 consecutive rule execution misses + // to be included in the number of signals generated. + const { maxCatchup, ratio, gapDiffInUnits } = getGapMaxCatchupRatio({ + logger, + buildRuleMessage, + previousStartedAt, + unit, + ruleParamsFrom, + interval, + }); + logger.debug(buildRuleMessage(`maxCatchup: ${maxCatchup}, ratio: ${ratio}`)); + if (maxCatchup == null || ratio == null || gapDiffInUnits == null) { + throw new Error( + buildRuleMessage('failed to calculate maxCatchup, ratio, or gapDiffInUnits') + ); + } + let tempTo = dateMath.parse(ruleParamsFrom); + if (tempTo == null) { + // return an error + throw new Error(buildRuleMessage('dateMath parse failed')); + } - let tempTo = dateMath.parse(ruleParamsFrom); - if (tempTo == null) { - // return an error - throw new Error('dateMath parse failed'); + let beforeMutatedFrom: moment.Moment | undefined; + while (totalToFromTuples.length < maxCatchup) { + // if maxCatchup is less than 1, we calculate the 'from' differently + // and maxSignals becomes some less amount of maxSignals + // in order to maintain maxSignals per full rule interval. + if (maxCatchup > 0 && maxCatchup < 1) { + totalToFromTuples.push({ + to: tempTo.clone(), + from: tempTo.clone().subtract(gapDiffInUnits, momentUnit), + maxSignals: ruleParamsMaxSignals * maxCatchup, + }); + break; } + const beforeMutatedTo = tempTo.clone(); - let beforeMutatedFrom: moment.Moment | undefined; - while (totalToFromTuples.length < maxCatchup) { - // if maxCatchup is less than 1, we calculate the 'from' differently - // and maxSignals becomes some less amount of maxSignals - // in order to maintain maxSignals per full rule interval. - if (maxCatchup > 0 && maxCatchup < 1) { - totalToFromTuples.push({ - to: tempTo.clone(), - from: tempTo.clone().subtract(Math.abs(gapDiffInUnits), momentUnit), - maxSignals: ruleParamsMaxSignals * maxCatchup, - }); - break; - } - const beforeMutatedTo = tempTo.clone(); - - // moment.subtract mutates the moment so we need to clone again.. - beforeMutatedFrom = tempTo.clone().subtract(intervalMoment, momentUnit); - const tuple = { - to: beforeMutatedTo, - from: beforeMutatedFrom, - maxSignals: ruleParamsMaxSignals, - }; - totalToFromTuples = [...totalToFromTuples, tuple]; - tempTo = beforeMutatedFrom; - } - totalToFromTuples = [ - { - to: dateMath.parse(ruleParamsTo), - from: dateMath.parse(ruleParamsFrom), - maxSignals: ruleParamsMaxSignals, - }, - ...totalToFromTuples, - ]; - } else { - logger.debug(buildRuleMessage('calculatedFromMoment was null or intervalMoment was null')); + // moment.subtract mutates the moment so we need to clone again.. + beforeMutatedFrom = tempTo.clone().subtract(intervalMoment, momentUnit); + const tuple = { + to: beforeMutatedTo, + from: beforeMutatedFrom, + maxSignals: ruleParamsMaxSignals, + }; + totalToFromTuples = [...totalToFromTuples, tuple]; + tempTo = beforeMutatedFrom; } + totalToFromTuples = [ + { + to: dateMath.parse(ruleParamsTo), + from: dateMath.parse(ruleParamsFrom), + maxSignals: ruleParamsMaxSignals, + }, + ...totalToFromTuples, + ]; } } else { totalToFromTuples = [ From b1433e6317b34e39c572df48d952c27e32eaec2b Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 14 Jul 2020 15:08:11 -0700 Subject: [PATCH 54/82] remove unnecessary context reference from trigger job (cherry picked from commit 817fdf9b439e85c3ddfda126b3efb4e45c36006b) --- .ci/Jenkinsfile_baseline_trigger | 2 +- vars/githubCommitStatus.groovy | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.ci/Jenkinsfile_baseline_trigger b/.ci/Jenkinsfile_baseline_trigger index 05daeebdc058c..752334dbb6cc9 100644 --- a/.ci/Jenkinsfile_baseline_trigger +++ b/.ci/Jenkinsfile_baseline_trigger @@ -21,7 +21,7 @@ withGithubCredentials { commits.take(MAXIMUM_COMMITS_TO_BUILD).each { commit -> catchErrors { - githubCommitStatus.create(commit, 'pending', 'Baseline started.', context = 'kibana-ci-baseline') + githubCommitStatus.create(commit, 'pending', 'Baseline started.', 'kibana-ci-baseline') build( propagate: false, diff --git a/vars/githubCommitStatus.groovy b/vars/githubCommitStatus.groovy index 4cd4228d55f03..17d3c234f6928 100644 --- a/vars/githubCommitStatus.groovy +++ b/vars/githubCommitStatus.groovy @@ -35,7 +35,12 @@ def onFinish() { // state: error|failure|pending|success def create(sha, state, description, context = 'kibana-ci') { withGithubCredentials { - return githubApi.post("repos/elastic/kibana/statuses/${sha}", [ state: state, description: description, context: context, target_url: env.BUILD_URL ]) + return githubApi.post("repos/elastic/kibana/statuses/${sha}", [ + state: state, + description: description, + context: context, + target_url: env.BUILD_URL + ]) } } From e318ea76dc290442d385f0134aaada2cbb52d2bd Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 14 Jul 2020 15:10:01 -0700 Subject: [PATCH 55/82] fix triggered job name --- .ci/Jenkinsfile_baseline_trigger | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/Jenkinsfile_baseline_trigger b/.ci/Jenkinsfile_baseline_trigger index 752334dbb6cc9..cc9fb47ca4993 100644 --- a/.ci/Jenkinsfile_baseline_trigger +++ b/.ci/Jenkinsfile_baseline_trigger @@ -26,7 +26,7 @@ withGithubCredentials { build( propagate: false, wait: false, - job: 'elastic+kibana+baseline', + job: 'elastic+kibana+baseline-capture', parameters: [ string(name: 'branch_specifier', value: branch), string(name: 'commit', value: commit), From 1f340969eeb2a5f977e1bad28daab5f2fb96a3a0 Mon Sep 17 00:00:00 2001 From: Lee Drengenberg Date: Tue, 14 Jul 2020 17:28:03 -0500 Subject: [PATCH 56/82] re-fix navigate path for master add SAML login to login_page (#71337) --- test/functional/page_objects/login_page.ts | 60 +++++++++++++++++-- ...onfig.stack_functional_integration_base.js | 8 ++- .../functional/apps/sample_data/e_commerce.js | 2 +- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/test/functional/page_objects/login_page.ts b/test/functional/page_objects/login_page.ts index c84f47a342155..350ab8be1a274 100644 --- a/test/functional/page_objects/login_page.ts +++ b/test/functional/page_objects/login_page.ts @@ -7,26 +7,76 @@ * not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + *    http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the + * KIND, either express or implied.  See the License for the * specific language governing permissions and limitations * under the License. */ +import { delay } from 'bluebird'; import { FtrProviderContext } from '../ftr_provider_context'; export function LoginPageProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); + const log = getService('log'); + const find = getService('find'); + + const regularLogin = async (user: string, pwd: string) => { + await testSubjects.setValue('loginUsername', user); + await testSubjects.setValue('loginPassword', pwd); + await testSubjects.click('loginSubmit'); + await find.waitForDeletedByCssSelector('.kibanaWelcomeLogo'); + await find.byCssSelector('[data-test-subj="kibanaChrome"]', 60000); // 60 sec waiting + }; + + const samlLogin = async (user: string, pwd: string) => { + try { + await find.clickByButtonText('Login using SAML'); + await find.setValue('input[name="email"]', user); + await find.setValue('input[type="password"]', pwd); + await find.clickByCssSelector('.auth0-label-submit'); + await find.byCssSelector('[data-test-subj="kibanaChrome"]', 60000); // 60 sec waiting + } catch (err) { + log.debug(`${err} \nFailed to find Auth0 login page, trying the Auth0 last login page`); + await find.clickByCssSelector('.auth0-lock-social-button'); + } + }; class LoginPage { async login(user: string, pwd: string) { - await testSubjects.setValue('loginUsername', user); - await testSubjects.setValue('loginPassword', pwd); - await testSubjects.click('loginSubmit'); + if ( + process.env.VM === 'ubuntu18_deb_oidc' || + process.env.VM === 'ubuntu16_deb_desktop_saml' + ) { + await samlLogin(user, pwd); + return; + } + + await regularLogin(user, pwd); + } + + async logoutLogin(user: string, pwd: string) { + await this.logout(); + await this.sleep(3002); + await this.login(user, pwd); + } + + async logout() { + await testSubjects.click('userMenuButton'); + await this.sleep(500); + await testSubjects.click('logoutLink'); + log.debug('### found and clicked log out--------------------------'); + await this.sleep(8002); + } + + async sleep(sleepMilliseconds: number) { + log.debug(`... sleep(${sleepMilliseconds}) start`); + await delay(sleepMilliseconds); + log.debug(`... sleep(${sleepMilliseconds}) end`); } } diff --git a/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js b/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js index a34d158496ba0..96d338a04b01b 100644 --- a/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js +++ b/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js @@ -12,12 +12,16 @@ import { esTestConfig, kbnTestConfig } from '@kbn/test'; const reportName = 'Stack Functional Integration Tests'; const testsFolder = '../test/functional/apps'; -const stateFilePath = '../../../../../integration-test/qa/envvars.sh'; -const prepend = (testFile) => require.resolve(`${testsFolder}/${testFile}`); const log = new ToolingLog({ level: 'info', writeTo: process.stdout, }); +log.info(`WORKSPACE in config file ${process.env.WORKSPACE}`); +const stateFilePath = process.env.WORKSPACE + ? `${process.env.WORKSPACE}/qa/envvars.sh` + : '../../../../../integration-test/qa/envvars.sh'; + +const prepend = (testFile) => require.resolve(`${testsFolder}/${testFile}`); export default async ({ readConfigFile }) => { const defaultConfigs = await readConfigFile(require.resolve('../../functional/config')); diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/sample_data/e_commerce.js b/x-pack/test/stack_functional_integration/test/functional/apps/sample_data/e_commerce.js index 306f30133f6ee..0286f6984e89e 100644 --- a/x-pack/test/stack_functional_integration/test/functional/apps/sample_data/e_commerce.js +++ b/x-pack/test/stack_functional_integration/test/functional/apps/sample_data/e_commerce.js @@ -12,7 +12,7 @@ export default function ({ getService, getPageObjects }) { before(async () => { await browser.setWindowSize(1200, 800); - await PageObjects.common.navigateToUrl('home', '/home/tutorial_directory/sampleData', { + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { useActualUrl: true, insertTimestamp: false, }); From 654d4da90460f3038caf9a8ffba7255832362513 Mon Sep 17 00:00:00 2001 From: Brent Kimmel Date: Tue, 14 Jul 2020 18:51:59 -0400 Subject: [PATCH 57/82] [Security_Solution][Bug] Handle non-ecs categories in events (#71714) * Make resolver related event categories permissive --- .../resolver/store/data/reducer.test.ts | 9 + .../public/resolver/store/data/selectors.ts | 32 ++++ .../public/resolver/store/selectors.ts | 9 + .../public/resolver/view/panel.tsx | 3 +- .../panels/panel_content_related_list.tsx | 46 ++--- .../resolver/view/process_event_dot.tsx | 169 +----------------- 6 files changed, 69 insertions(+), 199 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts index 2f4cf161faa9b..edda2ef984a9e 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts @@ -166,6 +166,15 @@ describe('Resolver Data Middleware', () => { expect(selectedEventsForFirstChildNode).toBe(firstChildNodeInTree.relatedEvents); }); + it('should return related events for the category equal to the number of events of that type provided', () => { + const relatedEventsByCategory = selectors.relatedEventsByCategory(store.getState()); + const relatedEventsForOvercountedCategory = relatedEventsByCategory( + firstChildNodeInTree.id + )(categoryToOverCount); + expect(relatedEventsForOvercountedCategory.length).toBe( + eventStatsForFirstChildNode.byCategory[categoryToOverCount] - 1 + ); + }); it('should indicate the limit has been exceeded because the number of related events received for the category is less than what the stats count said it would be', () => { const selectedRelatedInfo = selectors.relatedEventInfoByEntityId(store.getState()); const shouldShowLimit = selectedRelatedInfo(firstChildNodeInTree.id) diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index 9f425217a8d3e..475546cfc3966 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -130,6 +130,38 @@ export function relatedEventsByEntityId(data: DataState): Map { + return defaultMemoize((ecsCategory: string) => { + const relatedById = relatedEventsByEntityId.get(entityId); + // With no related events, we can't return related by category + if (!relatedById) { + return []; + } + return relatedById.events.reduce( + (eventsByCategory: ResolverEvent[], candidate: ResolverEvent) => { + if ([candidate && allEventCategories(candidate)].flat().includes(ecsCategory)) { + eventsByCategory.push(candidate); + } + return eventsByCategory; + }, + [] + ); + }); + }); + } +); + /** * returns a map of entity_ids to booleans indicating if it is waiting on related event * A value of `undefined` can be interpreted as `not yet requested` diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index 64921d214cc1b..945b2bfed3cfb 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -100,6 +100,15 @@ export const relatedEventsByEntityId = composeSelectors( dataSelectors.relatedEventsByEntityId ); +/** + * Returns a function that returns a function (when supplied with an entity id for a node) + * that returns related events for a node that match an event.category (when supplied with the category) + */ +export const relatedEventsByCategory = composeSelectors( + dataStateSelector, + dataSelectors.relatedEventsByCategory +); + /** * Entity ids to booleans for waiting status */ diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx index 061531b82d935..47ce9b949fa59 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx @@ -7,7 +7,6 @@ import React, { memo, useMemo, useContext, useLayoutEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { EuiPanel } from '@elastic/eui'; -import { displayNameRecord } from './process_event_dot'; import * as selectors from '../store/selectors'; import { useResolverDispatch } from './use_resolver_dispatch'; import * as event from '../../../common/endpoint/models/event'; @@ -144,7 +143,7 @@ const PanelContent = memo(function PanelContent() { * | relateds list 1 type | entity_id of process | valid related event type | */ - if (crumbEvent in displayNameRecord && uiSelectedEvent) { + if (crumbEvent && crumbEvent.length && uiSelectedEvent) { return 'processEventListNarrowedByType'; } } diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_list.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_list.tsx index 591432e1f9f9f..0878ead72b2a4 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_list.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_list.tsx @@ -164,9 +164,6 @@ export const ProcessEventListNarrowedByType = memo(function ProcessEventListNarr const relatedsReadyMap = useSelector(selectors.relatedEventsReady); const relatedsReady = relatedsReadyMap.get(processEntityId); - const relatedEventsForThisProcess = useSelector(selectors.relatedEventsByEntityId).get( - processEntityId - ); const dispatch = useResolverDispatch(); useEffect(() => { @@ -189,39 +186,30 @@ export const ProcessEventListNarrowedByType = memo(function ProcessEventListNarr ]; }, [pushToQueryParams, eventsString]); - const relatedEventsToDisplay = useMemo(() => { - return relatedEventsForThisProcess?.events || []; - }, [relatedEventsForThisProcess?.events]); + const relatedByCategory = useSelector(selectors.relatedEventsByCategory); /** * A list entry will be displayed for each of these */ const matchingEventEntries: MatchingEventEntry[] = useMemo(() => { - const relateds = relatedEventsToDisplay - .reduce((a: ResolverEvent[], candidate) => { - if (event.primaryEventCategory(candidate) === eventType) { - a.push(candidate); - } - return a; - }, []) - .map((resolverEvent) => { - const eventTime = event.eventTimestamp(resolverEvent); - const formattedDate = typeof eventTime === 'undefined' ? '' : formatDate(eventTime); - const entityId = event.eventId(resolverEvent); + const relateds = relatedByCategory(processEntityId)(eventType).map((resolverEvent) => { + const eventTime = event.eventTimestamp(resolverEvent); + const formattedDate = typeof eventTime === 'undefined' ? '' : formatDate(eventTime); + const entityId = event.eventId(resolverEvent); - return { - formattedDate, - eventCategory: `${eventType}`, - eventType: `${event.ecsEventType(resolverEvent)}`, - name: event.descriptiveName(resolverEvent), - entityId, - setQueryParams: () => { - pushToQueryParams({ crumbId: entityId, crumbEvent: processEntityId }); - }, - }; - }); + return { + formattedDate, + eventCategory: `${eventType}`, + eventType: `${event.ecsEventType(resolverEvent)}`, + name: event.descriptiveName(resolverEvent), + entityId, + setQueryParams: () => { + pushToQueryParams({ crumbId: entityId, crumbEvent: processEntityId }); + }, + }; + }); return relateds; - }, [relatedEventsToDisplay, eventType, processEntityId, pushToQueryParams]); + }, [relatedByCategory, eventType, processEntityId, pushToQueryParams]); const crumbs = useMemo(() => { return [ diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 17e7d3df42931..e20f06ccf0f72 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -8,7 +8,6 @@ import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; -import { i18n } from '@kbn/i18n'; import { htmlIdGenerator, EuiButton, EuiI18nNumber, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useSelector } from 'react-redux'; import { NodeSubMenu, subMenuAssets } from './submenu'; @@ -21,172 +20,6 @@ import * as eventModel from '../../../common/endpoint/models/event'; import * as selectors from '../store/selectors'; import { useResolverQueryParams } from './use_resolver_query_params'; -/** - * A record of all known event types (in schema format) to translations - */ -export const displayNameRecord = { - application: i18n.translate( - 'xpack.securitySolution.endpoint.resolver.applicationEventTypeDisplayName', - { - defaultMessage: 'Application', - } - ), - apm: i18n.translate('xpack.securitySolution.endpoint.resolver.apmEventTypeDisplayName', { - defaultMessage: 'APM', - }), - audit: i18n.translate('xpack.securitySolution.endpoint.resolver.auditEventTypeDisplayName', { - defaultMessage: 'Audit', - }), - authentication: i18n.translate( - 'xpack.securitySolution.endpoint.resolver.authenticationEventTypeDisplayName', - { - defaultMessage: 'Authentication', - } - ), - certificate: i18n.translate( - 'xpack.securitySolution.endpoint.resolver.certificateEventTypeDisplayName', - { - defaultMessage: 'Certificate', - } - ), - cloud: i18n.translate('xpack.securitySolution.endpoint.resolver.cloudEventTypeDisplayName', { - defaultMessage: 'Cloud', - }), - database: i18n.translate( - 'xpack.securitySolution.endpoint.resolver.databaseEventTypeDisplayName', - { - defaultMessage: 'Database', - } - ), - driver: i18n.translate('xpack.securitySolution.endpoint.resolver.driverEventTypeDisplayName', { - defaultMessage: 'Driver', - }), - email: i18n.translate('xpack.securitySolution.endpoint.resolver.emailEventTypeDisplayName', { - defaultMessage: 'Email', - }), - file: i18n.translate('xpack.securitySolution.endpoint.resolver.fileEventTypeDisplayName', { - defaultMessage: 'File', - }), - host: i18n.translate('xpack.securitySolution.endpoint.resolver.hostEventTypeDisplayName', { - defaultMessage: 'Host', - }), - iam: i18n.translate('xpack.securitySolution.endpoint.resolver.iamEventTypeDisplayName', { - defaultMessage: 'IAM', - }), - iam_group: i18n.translate( - 'xpack.securitySolution.endpoint.resolver.iam_groupEventTypeDisplayName', - { - defaultMessage: 'IAM Group', - } - ), - intrusion_detection: i18n.translate( - 'xpack.securitySolution.endpoint.resolver.intrusion_detectionEventTypeDisplayName', - { - defaultMessage: 'Intrusion Detection', - } - ), - malware: i18n.translate('xpack.securitySolution.endpoint.resolver.malwareEventTypeDisplayName', { - defaultMessage: 'Malware', - }), - network_flow: i18n.translate( - 'xpack.securitySolution.endpoint.resolver.network_flowEventTypeDisplayName', - { - defaultMessage: 'Network Flow', - } - ), - network: i18n.translate('xpack.securitySolution.endpoint.resolver.networkEventTypeDisplayName', { - defaultMessage: 'Network', - }), - package: i18n.translate('xpack.securitySolution.endpoint.resolver.packageEventTypeDisplayName', { - defaultMessage: 'Package', - }), - process: i18n.translate('xpack.securitySolution.endpoint.resolver.processEventTypeDisplayName', { - defaultMessage: 'Process', - }), - registry: i18n.translate( - 'xpack.securitySolution.endpoint.resolver.registryEventTypeDisplayName', - { - defaultMessage: 'Registry', - } - ), - session: i18n.translate('xpack.securitySolution.endpoint.resolver.sessionEventTypeDisplayName', { - defaultMessage: 'Session', - }), - service: i18n.translate('xpack.securitySolution.endpoint.resolver.serviceEventTypeDisplayName', { - defaultMessage: 'Service', - }), - socket: i18n.translate('xpack.securitySolution.endpoint.resolver.socketEventTypeDisplayName', { - defaultMessage: 'Socket', - }), - vulnerability: i18n.translate( - 'xpack.securitySolution.endpoint.resolver.vulnerabilityEventTypeDisplayName', - { - defaultMessage: 'Vulnerability', - } - ), - web: i18n.translate('xpack.securitySolution.endpoint.resolver.webEventTypeDisplayName', { - defaultMessage: 'Web', - }), - alert: i18n.translate('xpack.securitySolution.endpoint.resolver.alertEventTypeDisplayName', { - defaultMessage: 'Alert', - }), - security: i18n.translate( - 'xpack.securitySolution.endpoint.resolver.securityEventTypeDisplayName', - { - defaultMessage: 'Security', - } - ), - dns: i18n.translate('xpack.securitySolution.endpoint.resolver.dnsEventTypeDisplayName', { - defaultMessage: 'DNS', - }), - clr: i18n.translate('xpack.securitySolution.endpoint.resolver.clrEventTypeDisplayName', { - defaultMessage: 'CLR', - }), - image_load: i18n.translate( - 'xpack.securitySolution.endpoint.resolver.image_loadEventTypeDisplayName', - { - defaultMessage: 'Image Load', - } - ), - powershell: i18n.translate( - 'xpack.securitySolution.endpoint.resolver.powershellEventTypeDisplayName', - { - defaultMessage: 'Powershell', - } - ), - wmi: i18n.translate('xpack.securitySolution.endpoint.resolver.wmiEventTypeDisplayName', { - defaultMessage: 'WMI', - }), - api: i18n.translate('xpack.securitySolution.endpoint.resolver.apiEventTypeDisplayName', { - defaultMessage: 'API', - }), - user: i18n.translate('xpack.securitySolution.endpoint.resolver.userEventTypeDisplayName', { - defaultMessage: 'User', - }), -} as const; - -const unknownEventTypeMessage = i18n.translate( - 'xpack.securitySolution.endpoint.resolver.userEventTypeDisplayUnknown', - { - defaultMessage: 'Unknown', - } -); - -type EventDisplayName = typeof displayNameRecord[keyof typeof displayNameRecord] & - typeof unknownEventTypeMessage; - -/** - * Take a `schemaName` and return a translation. - */ -const schemaNameTranslation: ( - schemaName: string -) => EventDisplayName = function nameInSchemaToDisplayName(schemaName) { - if (schemaName in displayNameRecord) { - return displayNameRecord[schemaName as keyof typeof displayNameRecord]; - } - return unknownEventTypeMessage; -}; - interface StyledActionsContainer { readonly color: string; readonly fontSize: number; @@ -437,7 +270,7 @@ const UnstyledProcessEventDot = React.memo( )) { relatedStatsList.push({ prefix: , - optionTitle: schemaNameTranslation(category), + optionTitle: category, action: () => { dispatch({ type: 'userSelectedRelatedEventCategory', From 86733f60ffa048738fdf93358d9ceee6ca718dd6 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 14 Jul 2020 16:02:49 -0700 Subject: [PATCH 58/82] [tests] Temporarily skipped to promote snapshot Will be re-enabled in #71727 Signed-off-by: Tyler Smalley --- x-pack/test/api_integration/apis/fleet/unenroll_agent.ts | 4 +++- .../apps/endpoint/policy_details.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts index bc6c44e590cc4..76cd48b63e869 100644 --- a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts +++ b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts @@ -16,7 +16,9 @@ export default function (providerContext: FtrProviderContext) { const supertest = getService('supertest'); const esClient = getService('es'); - describe('fleet_unenroll_agent', () => { + // Temporarily skipped to promote snapshot + // Re-enabled in https://github.com/elastic/kibana/pull/71727 + describe.skip('fleet_unenroll_agent', () => { let accessAPIKeyId: string; let outputAPIKeyId: string; before(async () => { diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index cf76f297d83be..0c9a86449506b 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -19,7 +19,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const policyTestResources = getService('policyTestResources'); - describe('When on the Endpoint Policy Details Page', function () { + // Temporarily skipped to promote snapshot + // Re-enabled in https://github.com/elastic/kibana/pull/71727 + describe.skip('When on the Endpoint Policy Details Page', function () { this.tags(['ciGroup7']); describe('with an invalid policy id', () => { From de4d65cc75611ddbe3e98c4972222f99288c573d Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Tue, 14 Jul 2020 19:41:13 -0400 Subject: [PATCH 59/82] [Maps] Remove .mvt feature flag (#71779) The layer wizard to add 3rd party .mvt tiles now shows by default. --- x-pack/plugins/maps/config.ts | 3 --- .../maps/public/classes/layers/load_layer_wizards.ts | 7 +------ x-pack/plugins/maps/public/kibana_services.d.ts | 1 - x-pack/plugins/maps/public/kibana_services.js | 1 - x-pack/plugins/maps/server/index.ts | 1 - 5 files changed, 1 insertion(+), 12 deletions(-) diff --git a/x-pack/plugins/maps/config.ts b/x-pack/plugins/maps/config.ts index 8bb0b7551b0e1..b97c09d9b86ba 100644 --- a/x-pack/plugins/maps/config.ts +++ b/x-pack/plugins/maps/config.ts @@ -11,7 +11,6 @@ export interface MapsConfigType { showMapVisualizationTypes: boolean; showMapsInspectorAdapter: boolean; preserveDrawingBuffer: boolean; - enableVectorTiles: boolean; } export const configSchema = schema.object({ @@ -21,8 +20,6 @@ export const configSchema = schema.object({ showMapsInspectorAdapter: schema.boolean({ defaultValue: false }), // flag used in functional testing preserveDrawingBuffer: schema.boolean({ defaultValue: false }), - // flag used to enable/disable vector-tiles - enableVectorTiles: schema.boolean({ defaultValue: false }), }); export type MapsXPackConfig = TypeOf; diff --git a/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts b/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts index 9af1684c0bac1..eaef7931b5e6c 100644 --- a/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts +++ b/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts @@ -27,7 +27,6 @@ import { mvtVectorSourceWizardConfig } from '../sources/mvt_single_layer_vector_ import { ObservabilityLayerWizardConfig } from './solution_layers/observability'; import { SecurityLayerWizardConfig } from './solution_layers/security'; import { choroplethLayerWizardConfig } from './choropleth_layer_wizard'; -import { getEnableVectorTiles } from '../../kibana_services'; let registered = false; export function registerLayerWizards() { @@ -60,10 +59,6 @@ export function registerLayerWizards() { // @ts-ignore registerLayerWizard(wmsLayerWizardConfig); - if (getEnableVectorTiles()) { - // eslint-disable-next-line no-console - console.warn('Vector tiles are an experimental feature and should not be used in production.'); - registerLayerWizard(mvtVectorSourceWizardConfig); - } + registerLayerWizard(mvtVectorSourceWizardConfig); registered = true; } diff --git a/x-pack/plugins/maps/public/kibana_services.d.ts b/x-pack/plugins/maps/public/kibana_services.d.ts index d4a7fa5d50af8..974bccf4942f3 100644 --- a/x-pack/plugins/maps/public/kibana_services.d.ts +++ b/x-pack/plugins/maps/public/kibana_services.d.ts @@ -47,7 +47,6 @@ export function getEnabled(): boolean; export function getShowMapVisualizationTypes(): boolean; export function getShowMapsInspectorAdapter(): boolean; export function getPreserveDrawingBuffer(): boolean; -export function getEnableVectorTiles(): boolean; export function getProxyElasticMapsServiceInMaps(): boolean; export function getIsGoldPlus(): boolean; diff --git a/x-pack/plugins/maps/public/kibana_services.js b/x-pack/plugins/maps/public/kibana_services.js index 97d7f0c66c629..53e128f94dfb6 100644 --- a/x-pack/plugins/maps/public/kibana_services.js +++ b/x-pack/plugins/maps/public/kibana_services.js @@ -152,7 +152,6 @@ export const getEnabled = () => getMapAppConfig().enabled; export const getShowMapVisualizationTypes = () => getMapAppConfig().showMapVisualizationTypes; export const getShowMapsInspectorAdapter = () => getMapAppConfig().showMapsInspectorAdapter; export const getPreserveDrawingBuffer = () => getMapAppConfig().preserveDrawingBuffer; -export const getEnableVectorTiles = () => getMapAppConfig().enableVectorTiles; // map.* kibana.yml settings from maps_legacy plugin that are shared between OSS map visualizations and maps app let kibanaCommonConfig; diff --git a/x-pack/plugins/maps/server/index.ts b/x-pack/plugins/maps/server/index.ts index a73ba91098e90..19ab532262971 100644 --- a/x-pack/plugins/maps/server/index.ts +++ b/x-pack/plugins/maps/server/index.ts @@ -15,7 +15,6 @@ export const config: PluginConfigDescriptor = { enabled: true, showMapVisualizationTypes: true, showMapsInspectorAdapter: true, - enableVectorTiles: true, preserveDrawingBuffer: true, }, schema: configSchema, From 58b4127b68cdc976da148b9f4334590c50f1bf6a Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 14 Jul 2020 20:13:44 -0400 Subject: [PATCH 60/82] Unskip functional tests for feature controls (#71173) * Unskip functional tests for feature controls * Update Maps test * Update test title * Fix hidden case-sensitive issue in saved queries * Fix test separation issues * Improve saved query retry logic Co-authored-by: Elastic Machine --- .../saved_query_management_component.ts | 15 +++- .../feature_controls/dashboard_security.ts | 73 +++++++++++++------ .../feature_controls/discover_security.ts | 47 ++++++++---- .../maps/feature_controls/maps_security.ts | 58 +++++++++------ .../functional/apps/maps/full_screen_mode.js | 4 +- .../feature_controls/visualize_security.ts | 53 ++++++++------ .../feature_controls/security/data.json | 2 +- .../feature_controls/security/data.json | 2 +- .../es_archives/maps/kibana/data.json | 2 +- .../es_archives/visualize/default/data.json | 2 +- .../test/functional/page_objects/gis_page.js | 5 +- x-pack/test/functional/services/user_menu.js | 6 +- .../es_archives/global_search/basic/data.json | 2 +- 13 files changed, 174 insertions(+), 97 deletions(-) diff --git a/test/functional/services/saved_query_management_component.ts b/test/functional/services/saved_query_management_component.ts index 66bf15f3da53c..f600dba368485 100644 --- a/test/functional/services/saved_query_management_component.ts +++ b/test/functional/services/saved_query_management_component.ts @@ -20,11 +20,15 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; -export function SavedQueryManagementComponentProvider({ getService }: FtrProviderContext) { +export function SavedQueryManagementComponentProvider({ + getService, + getPageObjects, +}: FtrProviderContext) { const testSubjects = getService('testSubjects'); const queryBar = getService('queryBar'); const retry = getService('retry'); const config = getService('config'); + const PageObjects = getPageObjects(['common']); class SavedQueryManagementComponent { public async getCurrentlyLoadedQueryID() { @@ -105,7 +109,7 @@ export function SavedQueryManagementComponentProvider({ getService }: FtrProvide public async deleteSavedQuery(title: string) { await this.openSavedQueryManagementComponent(); await testSubjects.click(`~delete-saved-query-${title}-button`); - await testSubjects.click('confirmModalConfirmButton'); + await PageObjects.common.clickConfirmOnModal(); } async clearCurrentlyLoadedQuery() { @@ -169,8 +173,8 @@ export function SavedQueryManagementComponentProvider({ getService }: FtrProvide const isOpenAlready = await testSubjects.exists('saved-query-management-popover'); if (isOpenAlready) return; - await testSubjects.click('saved-query-management-popover-button'); await retry.waitFor('saved query management popover to have any text', async () => { + await testSubjects.click('saved-query-management-popover-button'); const queryText = await testSubjects.getVisibleText('saved-query-management-popover'); return queryText.length > 0; }); @@ -180,7 +184,10 @@ export function SavedQueryManagementComponentProvider({ getService }: FtrProvide const isOpenAlready = await testSubjects.exists('saved-query-management-popover'); if (!isOpenAlready) return; - await testSubjects.click('saved-query-management-popover-button'); + await retry.try(async () => { + await testSubjects.click('saved-query-management-popover-button'); + await testSubjects.missingOrFail('saved-query-management-popover'); + }); } async openSaveCurrentQueryModal() { diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts index f76bdbe5c10ca..505e35907bd80 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts @@ -29,8 +29,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const queryBar = getService('queryBar'); const savedQueryManagementComponent = getService('savedQueryManagementComponent'); - // FLAKY: https://github.com/elastic/kibana/issues/44631 - describe.skip('dashboard security', () => { + describe('dashboard feature controls security', () => { before(async () => { await esArchiver.load('dashboard/feature_controls/security'); await esArchiver.loadIfNeeded('logstash_functional'); @@ -84,7 +83,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows dashboard navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map((link) => link.text)).to.eql(['Dashboard', 'Stack Management']); + expect(navLinks.map((link) => link.text)).to.contain('Dashboard'); }); it(`landing page shows "Create new Dashboard" button`, async () => { @@ -106,9 +105,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await globalNav.badgeMissingOrFail(); }); - it(`create new dashboard shows addNew button`, async () => { + // Can't figure out how to get this test to pass + it.skip(`create new dashboard shows addNew button`, async () => { await PageObjects.common.navigateToActualUrl( - 'kibana', + 'dashboard', DashboardConstants.CREATE_NEW_DASHBOARD_URL, { ensureCurrentUrl: false, @@ -204,33 +204,48 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await panelActions.expectExistsEditPanelAction(); }); - it('allow saving via the saved query management component popover with no query loaded', async () => { + it('allows saving via the saved query management component popover with no saved query loaded', async () => { + await queryBar.setQuery('response:200'); await savedQueryManagementComponent.saveNewQuery('foo', 'bar', true, false); await savedQueryManagementComponent.savedQueryExistOrFail('foo'); - }); + await savedQueryManagementComponent.closeSavedQueryManagementComponent(); - it('allow saving a currently loaded saved query as a new query via the saved query management component ', async () => { - await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery( - 'foo2', - 'bar2', - true, - false - ); - await savedQueryManagementComponent.savedQueryExistOrFail('foo2'); + await savedQueryManagementComponent.deleteSavedQuery('foo'); + await savedQueryManagementComponent.savedQueryMissingOrFail('foo'); }); it('allow saving changes to a currently loaded query via the saved query management component', async () => { + await savedQueryManagementComponent.loadSavedQuery('OKJpgs'); await queryBar.setQuery('response:404'); - await savedQueryManagementComponent.updateCurrentlyLoadedQuery('bar2', false, false); + await savedQueryManagementComponent.updateCurrentlyLoadedQuery( + 'new description', + true, + false + ); await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); - await savedQueryManagementComponent.loadSavedQuery('foo2'); + await savedQueryManagementComponent.loadSavedQuery('OKJpgs'); const queryString = await queryBar.getQueryString(); expect(queryString).to.eql('response:404'); + + // Reset after changing + await queryBar.setQuery('response:200'); + await savedQueryManagementComponent.updateCurrentlyLoadedQuery( + 'Ok responses for jpg files', + true, + false + ); }); - it('allows deleting saved queries in the saved query management component ', async () => { - await savedQueryManagementComponent.deleteSavedQuery('foo2'); - await savedQueryManagementComponent.savedQueryMissingOrFail('foo2'); + it('allow saving currently loaded query as a copy', async () => { + await savedQueryManagementComponent.loadSavedQuery('OKJpgs'); + await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery( + 'ok2', + 'description', + true, + false + ); + await savedQueryManagementComponent.savedQueryExistOrFail('ok2'); + await savedQueryManagementComponent.deleteSavedQuery('ok2'); }); }); @@ -272,7 +287,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows dashboard navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Dashboard', 'Stack Management']); + expect(navLinks).to.contain('Dashboard'); }); it(`landing page doesn't show "Create new Dashboard" button`, async () => { @@ -291,10 +306,19 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`shows read-only badge`, async () => { + await PageObjects.common.navigateToActualUrl( + 'dashboard', + DashboardConstants.LANDING_PAGE_PATH, + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); await globalNav.badgeExistsOrFail('Read only'); }); - it(`create new dashboard redirects to the home page`, async () => { + // Has this behavior changed? + it.skip(`create new dashboard redirects to the home page`, async () => { await PageObjects.common.navigateToActualUrl( 'dashboard', DashboardConstants.CREATE_NEW_DASHBOARD_URL, @@ -391,7 +415,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows dashboard navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Dashboard', 'Stack Management']); + expect(navLinks).to.contain('Dashboard'); }); it(`landing page doesn't show "Create new Dashboard" button`, async () => { @@ -411,7 +435,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await globalNav.badgeExistsOrFail('Read only'); }); - it(`create new dashboard redirects to the home page`, async () => { + // Has this behavior changed? + it.skip(`create new dashboard redirects to the home page`, async () => { await PageObjects.common.navigateToActualUrl( 'dashboard', DashboardConstants.CREATE_NEW_DASHBOARD_URL, diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts index 03a5cc6ac8fa0..8be4349762808 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts @@ -28,7 +28,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); } - describe('security', () => { + describe('discover feature controls security', () => { before(async () => { await esArchiver.load('discover/feature_controls/security'); await esArchiver.loadIfNeeded('logstash_functional'); @@ -101,33 +101,48 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.share.clickShareTopNavButton(); }); - it('allow saving via the saved query management component popover with no query loaded', async () => { + it('allows saving via the saved query management component popover with no saved query loaded', async () => { + await queryBar.setQuery('response:200'); await savedQueryManagementComponent.saveNewQuery('foo', 'bar', true, false); await savedQueryManagementComponent.savedQueryExistOrFail('foo'); - }); + await savedQueryManagementComponent.closeSavedQueryManagementComponent(); - it('allow saving a currently loaded saved query as a new query via the saved query management component ', async () => { - await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery( - 'foo2', - 'bar2', - true, - false - ); - await savedQueryManagementComponent.savedQueryExistOrFail('foo2'); + await savedQueryManagementComponent.deleteSavedQuery('foo'); + await savedQueryManagementComponent.savedQueryMissingOrFail('foo'); }); it('allow saving changes to a currently loaded query via the saved query management component', async () => { + await savedQueryManagementComponent.loadSavedQuery('OKJpgs'); await queryBar.setQuery('response:404'); - await savedQueryManagementComponent.updateCurrentlyLoadedQuery('bar2', false, false); + await savedQueryManagementComponent.updateCurrentlyLoadedQuery( + 'new description', + true, + false + ); await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); - await savedQueryManagementComponent.loadSavedQuery('foo2'); + await savedQueryManagementComponent.loadSavedQuery('OKJpgs'); const queryString = await queryBar.getQueryString(); expect(queryString).to.eql('response:404'); + + // Reset after changing + await queryBar.setQuery('response:200'); + await savedQueryManagementComponent.updateCurrentlyLoadedQuery( + 'Ok responses for jpg files', + true, + false + ); }); - it('allows deleting saved queries in the saved query management component ', async () => { - await savedQueryManagementComponent.deleteSavedQuery('foo2'); - await savedQueryManagementComponent.savedQueryMissingOrFail('foo2'); + it('allow saving currently loaded query as a copy', async () => { + await savedQueryManagementComponent.loadSavedQuery('OKJpgs'); + await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery( + 'ok2', + 'description', + true, + false + ); + await savedQueryManagementComponent.savedQueryExistOrFail('ok2'); + await savedQueryManagementComponent.deleteSavedQuery('ok2'); }); }); diff --git a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts index 2449430ac85c2..f480f1f0ae24a 100644 --- a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts +++ b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts @@ -16,8 +16,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const queryBar = getService('queryBar'); const savedQueryManagementComponent = getService('savedQueryManagementComponent'); - // FLAKY: https://github.com/elastic/kibana/issues/38414 - describe.skip('security feature controls', () => { + describe('maps security feature controls', () => { before(async () => { await esArchiver.loadIfNeeded('maps/data'); await esArchiver.load('maps/kibana'); @@ -25,6 +24,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { after(async () => { await esArchiver.unload('maps/kibana'); + // logout, so the other tests don't accidentally run as the custom users we're testing below + await PageObjects.security.forceLogout(); }); describe('global maps all privileges', () => { @@ -83,35 +84,49 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await globalNav.badgeMissingOrFail(); }); - it('allows saving via the saved query management component popover with no query loaded', async () => { + it('allows saving via the saved query management component popover with no saved query loaded', async () => { await PageObjects.maps.openNewMap(); await queryBar.setQuery('response:200'); await savedQueryManagementComponent.saveNewQuery('foo', 'bar', true, false); await savedQueryManagementComponent.savedQueryExistOrFail('foo'); - }); + await savedQueryManagementComponent.closeSavedQueryManagementComponent(); - it('allows saving a currently loaded saved query as a new query via the saved query management component ', async () => { - await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery( - 'foo2', - 'bar2', - true, - false - ); - await savedQueryManagementComponent.savedQueryExistOrFail('foo2'); + await savedQueryManagementComponent.deleteSavedQuery('foo'); + await savedQueryManagementComponent.savedQueryMissingOrFail('foo'); }); it('allow saving changes to a currently loaded query via the saved query management component', async () => { + await savedQueryManagementComponent.loadSavedQuery('OKJpgs'); await queryBar.setQuery('response:404'); - await savedQueryManagementComponent.updateCurrentlyLoadedQuery('bar2', false, false); + await savedQueryManagementComponent.updateCurrentlyLoadedQuery( + 'new description', + true, + false + ); await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); - await savedQueryManagementComponent.loadSavedQuery('foo2'); + await savedQueryManagementComponent.loadSavedQuery('OKJpgs'); const queryString = await queryBar.getQueryString(); expect(queryString).to.eql('response:404'); + + // Reset after changing + await queryBar.setQuery('response:200'); + await savedQueryManagementComponent.updateCurrentlyLoadedQuery( + 'Ok responses for jpg files', + true, + false + ); }); - it('allows deleting saved queries in the saved query management component ', async () => { - await savedQueryManagementComponent.deleteSavedQuery('foo2'); - await savedQueryManagementComponent.savedQueryMissingOrFail('foo2'); + it('allow saving currently loaded query as a copy', async () => { + await savedQueryManagementComponent.loadSavedQuery('OKJpgs'); + await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery( + 'ok2', + 'description', + true, + false + ); + await savedQueryManagementComponent.savedQueryExistOrFail('ok2'); + await savedQueryManagementComponent.deleteSavedQuery('ok2'); }); }); @@ -144,6 +159,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expectSpaceSelector: false, } ); + + await PageObjects.maps.gotoMapListingPage(); }); after(async () => { @@ -157,16 +174,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`does not show create new button`, async () => { - await PageObjects.maps.gotoMapListingPage(); await PageObjects.maps.expectMissingCreateNewButton(); }); it(`does not allow a map to be deleted`, async () => { - await PageObjects.maps.gotoMapListingPage(); await testSubjects.missingOrFail('checkboxSelectAll'); }); - it(`shows read-only badge`, async () => { + // This behavior was removed when the Maps app was migrated to NP + it.skip(`shows read-only badge`, async () => { await globalNav.badgeExistsOrFail('Read only'); }); @@ -248,7 +264,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('does not show Maps navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Discover', 'Stack Management']); + expect(navLinks).to.not.contain('Maps'); }); it(`returns a 404`, async () => { diff --git a/x-pack/test/functional/apps/maps/full_screen_mode.js b/x-pack/test/functional/apps/maps/full_screen_mode.js index 7d89ff1454598..b4ea2b0baf255 100644 --- a/x-pack/test/functional/apps/maps/full_screen_mode.js +++ b/x-pack/test/functional/apps/maps/full_screen_mode.js @@ -9,9 +9,11 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['maps', 'common']); const retry = getService('retry'); + const esArchiver = getService('esArchiver'); - describe('full screen mode', () => { + describe('maps full screen mode', () => { before(async () => { + await esArchiver.loadIfNeeded('maps/data'); await PageObjects.maps.openNewMap(); }); diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts index cb641e78ead0a..49435df4f1c2a 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts @@ -26,7 +26,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const queryBar = getService('queryBar'); const savedQueryManagementComponent = getService('savedQueryManagementComponent'); - describe('feature controls security', () => { + describe('visualize feature controls security', () => { before(async () => { await esArchiver.load('visualize/default'); await esArchiver.loadIfNeeded('logstash_functional'); @@ -34,6 +34,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { after(async () => { await esArchiver.unload('visualize/default'); + // logout, so the other tests don't accidentally run as the custom users we're testing below + await PageObjects.security.forceLogout(); }); describe('global visualize all privileges', () => { @@ -124,41 +126,48 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.share.clickShareTopNavButton(); }); - // Flaky: https://github.com/elastic/kibana/issues/50018 - it.skip('allow saving via the saved query management component popover with no saved query loaded', async () => { + it('allows saving via the saved query management component popover with no saved query loaded', async () => { await queryBar.setQuery('response:200'); await savedQueryManagementComponent.saveNewQuery('foo', 'bar', true, false); await savedQueryManagementComponent.savedQueryExistOrFail('foo'); await savedQueryManagementComponent.closeSavedQueryManagementComponent(); + + await savedQueryManagementComponent.deleteSavedQuery('foo'); + await savedQueryManagementComponent.savedQueryMissingOrFail('foo'); }); - // Depends on skipped test above - it.skip('allow saving a currently loaded saved query as a new query via the saved query management component ', async () => { - await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery( - 'foo2', - 'bar2', + it('allow saving changes to a currently loaded query via the saved query management component', async () => { + await savedQueryManagementComponent.loadSavedQuery('OKJpgs'); + await queryBar.setQuery('response:404'); + await savedQueryManagementComponent.updateCurrentlyLoadedQuery( + 'new description', true, false ); - await savedQueryManagementComponent.savedQueryExistOrFail('foo2'); - await savedQueryManagementComponent.closeSavedQueryManagementComponent(); - }); - - // Depends on skipped test above - it.skip('allow saving changes to a currently loaded query via the saved query management component', async () => { - await savedQueryManagementComponent.loadSavedQuery('foo2'); - await queryBar.setQuery('response:404'); - await savedQueryManagementComponent.updateCurrentlyLoadedQuery('bar2', false, false); await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); - await savedQueryManagementComponent.loadSavedQuery('foo2'); + await savedQueryManagementComponent.loadSavedQuery('OKJpgs'); const queryString = await queryBar.getQueryString(); expect(queryString).to.eql('response:404'); + + // Reset after changing + await queryBar.setQuery('response:200'); + await savedQueryManagementComponent.updateCurrentlyLoadedQuery( + 'Ok responses for jpg files', + true, + false + ); }); - // Depends on skipped test above - it.skip('allows deleting saved queries in the saved query management component ', async () => { - await savedQueryManagementComponent.deleteSavedQuery('foo2'); - await savedQueryManagementComponent.savedQueryMissingOrFail('foo2'); + it('allow saving currently loaded query as a copy', async () => { + await savedQueryManagementComponent.loadSavedQuery('OKJpgs'); + await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery( + 'ok2', + 'description', + true, + false + ); + await savedQueryManagementComponent.savedQueryExistOrFail('ok2'); + await savedQueryManagementComponent.deleteSavedQuery('ok2'); }); }); diff --git a/x-pack/test/functional/es_archives/dashboard/feature_controls/security/data.json b/x-pack/test/functional/es_archives/dashboard/feature_controls/security/data.json index 4ff13f76bc43e..db4f27e42ee85 100644 --- a/x-pack/test/functional/es_archives/dashboard/feature_controls/security/data.json +++ b/x-pack/test/functional/es_archives/dashboard/feature_controls/security/data.json @@ -175,7 +175,7 @@ "value": { "index": ".kibana", "type": "doc", - "id": "query:okjpgs", + "id": "query:OKJpgs", "source": { "query": { "title": "OKJpgs", diff --git a/x-pack/test/functional/es_archives/discover/feature_controls/security/data.json b/x-pack/test/functional/es_archives/discover/feature_controls/security/data.json index 394393dce4962..03859300b5999 100644 --- a/x-pack/test/functional/es_archives/discover/feature_controls/security/data.json +++ b/x-pack/test/functional/es_archives/discover/feature_controls/security/data.json @@ -41,7 +41,7 @@ "value": { "index": ".kibana", "type": "doc", - "id": "query:okjpgs", + "id": "query:OKJpgs", "source": { "query": { "title": "OKJpgs", diff --git a/x-pack/test/functional/es_archives/maps/kibana/data.json b/x-pack/test/functional/es_archives/maps/kibana/data.json index c173d75075041..d2206009d9e65 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/data.json +++ b/x-pack/test/functional/es_archives/maps/kibana/data.json @@ -1022,7 +1022,7 @@ "type": "doc", "value": { "index": ".kibana", - "id": "query:okjpgs", + "id": "query:OKJpgs", "source": { "query": { "title": "OKJpgs", diff --git a/x-pack/test/functional/es_archives/visualize/default/data.json b/x-pack/test/functional/es_archives/visualize/default/data.json index b9a6e2346b482..f72a61c9e3b85 100644 --- a/x-pack/test/functional/es_archives/visualize/default/data.json +++ b/x-pack/test/functional/es_archives/visualize/default/data.json @@ -237,7 +237,7 @@ "value": { "index": ".kibana", "type": "doc", - "id": "query:okjpgs", + "id": "query:OKJpgs", "source": { "query": { "title": "OKJpgs", diff --git a/x-pack/test/functional/page_objects/gis_page.js b/x-pack/test/functional/page_objects/gis_page.js index 93b9d9b4b3f7b..ff50415d3066e 100644 --- a/x-pack/test/functional/page_objects/gis_page.js +++ b/x-pack/test/functional/page_objects/gis_page.js @@ -132,8 +132,9 @@ export function GisPageProvider({ getService, getPageObjects }) { async openNewMap() { log.debug(`Open new Map`); - await this.gotoMapListingPage(); - await testSubjects.click('newMapLink'); + // Navigate directly because we don't need to go through the map listing + // page. The listing page is skipped if there are no saved objects + await PageObjects.common.navigateToUrlWithBrowserHistory(APP_ID, '/map'); } async saveMap(name) { diff --git a/x-pack/test/functional/services/user_menu.js b/x-pack/test/functional/services/user_menu.js index c21d8fa538ab1..7cb4e9f4ddfa6 100644 --- a/x-pack/test/functional/services/user_menu.js +++ b/x-pack/test/functional/services/user_menu.js @@ -42,8 +42,10 @@ export function UserMenuProvider({ getService }) { return; } - await testSubjects.click('userMenuButton'); - await retry.waitFor('user menu opened', async () => await testSubjects.exists('userMenu')); + await retry.try(async () => { + await testSubjects.click('userMenuButton'); + await testSubjects.existOrFail('userMenu'); + }); } })(); } diff --git a/x-pack/test/plugin_functional/es_archives/global_search/basic/data.json b/x-pack/test/plugin_functional/es_archives/global_search/basic/data.json index f121f6859885b..97064dade912e 100644 --- a/x-pack/test/plugin_functional/es_archives/global_search/basic/data.json +++ b/x-pack/test/plugin_functional/es_archives/global_search/basic/data.json @@ -175,7 +175,7 @@ "value": { "index": ".kibana", "type": "doc", - "id": "query:okjpgs", + "id": "query:OKJpgs", "source": { "query": { "title": "OKJpgs", From a0f7dced1377ba84e11976c434f46b8cf484a871 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 14 Jul 2020 17:23:14 -0700 Subject: [PATCH 61/82] [kbn/optimizer] report sizes of assets produced by optimizer (#71319) * Revert "Report page load asset size (#66224)" This reverts commit 6f57fa0b2d12e87abab528b60a0da20495b1fb3e. * [kbn/optimizer] report sizes of assets produced by optimizer * coalese the fast-glob versions we're using to prevent additional installs * update kbn/pm dist * Revert "update kbn/pm dist" This reverts commit 68e24f0fadd545d649663fd5cbeb98c50ea84dc3. * Revert "coalese the fast-glob versions we're using to prevent additional installs" This reverts commit 4201fb60b66bf59dd9e50dab9d0ff66131df8974. * remove fast-glob, just recursivly call readdirSync() * update integration tests to use new chunk filename Co-authored-by: spalger Co-authored-by: Elastic Machine --- Jenkinsfile | 1 - .../basic_optimization.test.ts.snap | 2 +- .../basic_optimization.test.ts | 2 +- .../src/report_optimizer_stats.ts | 88 +- .../src/worker/webpack.config.ts | 3 +- packages/kbn-test/package.json | 2 - packages/kbn-test/src/index.ts | 1 - .../capture_page_load_metrics.ts | 81 - .../kbn-test/src/page_load_metrics/cli.ts | 90 - .../kbn-test/src/page_load_metrics/event.ts | 34 - .../kbn-test/src/page_load_metrics/index.ts | 21 - .../src/page_load_metrics/navigation.ts | 164 -- scripts/page_load_metrics.js | 21 - .../jenkins_xpack_page_load_metrics.sh | 9 - .../jenkins_xpack_visual_regression.sh | 3 + x-pack/.gitignore | 1 - x-pack/test/page_load_metrics/config.ts | 42 - .../es_archives/default/data.json.gz | Bin 1812 -> 0 bytes .../es_archives/default/mappings.json | 2402 ----------------- x-pack/test/page_load_metrics/runner.ts | 33 - yarn.lock | 83 +- 21 files changed, 87 insertions(+), 2996 deletions(-) delete mode 100644 packages/kbn-test/src/page_load_metrics/capture_page_load_metrics.ts delete mode 100644 packages/kbn-test/src/page_load_metrics/cli.ts delete mode 100644 packages/kbn-test/src/page_load_metrics/event.ts delete mode 100644 packages/kbn-test/src/page_load_metrics/index.ts delete mode 100644 packages/kbn-test/src/page_load_metrics/navigation.ts delete mode 100644 scripts/page_load_metrics.js delete mode 100644 test/scripts/jenkins_xpack_page_load_metrics.sh delete mode 100644 x-pack/test/page_load_metrics/config.ts delete mode 100644 x-pack/test/page_load_metrics/es_archives/default/data.json.gz delete mode 100644 x-pack/test/page_load_metrics/es_archives/default/mappings.json delete mode 100644 x-pack/test/page_load_metrics/runner.ts diff --git a/Jenkinsfile b/Jenkinsfile index f6f77ccae8427..69c61b5bfa988 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -42,7 +42,6 @@ kibanaPipeline(timeoutMinutes: 155, checkPrChanges: true, setCommitStatus: true) 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), 'xpack-accessibility': kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh'), 'xpack-savedObjectsFieldMetrics': kibanaPipeline.functionalTestProcess('xpack-savedObjectsFieldMetrics', './test/scripts/jenkins_xpack_saved_objects_field_metrics.sh'), - // 'xpack-pageLoadMetrics': kibanaPipeline.functionalTestProcess('xpack-pageLoadMetrics', './test/scripts/jenkins_xpack_page_load_metrics.sh'), 'xpack-securitySolutionCypress': { processNumber -> whenChanged(['x-pack/plugins/security_solution/', 'x-pack/test/security_solution_cypress/']) { kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypress', './test/scripts/jenkins_security_solution_cypress.sh')(processNumber) diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index c52873ab7ec20..109188e163d06 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -75,4 +75,4 @@ exports[`prepares assets for distribution: bar bundle 1`] = `"(function(modules) exports[`prepares assets for distribution: foo async bundle 1`] = `"(window[\\"foo_bundle_jsonpfunction\\"]=window[\\"foo_bundle_jsonpfunction\\"]||[]).push([[1],{3:function(module,__webpack_exports__,__webpack_require__){\\"use strict\\";__webpack_require__.r(__webpack_exports__);__webpack_require__.d(__webpack_exports__,\\"foo\\",(function(){return foo}));function foo(){}}}]);"`; -exports[`prepares assets for distribution: foo bundle 1`] = `"(function(modules){function webpackJsonpCallback(data){var chunkIds=data[0];var moreModules=data[1];var moduleId,chunkId,i=0,resolves=[];for(;i { expectFileMatchesSnapshotWithCompression('plugins/foo/target/public/foo.plugin.js', 'foo bundle'); expectFileMatchesSnapshotWithCompression( - 'plugins/foo/target/public/1.plugin.js', + 'plugins/foo/target/public/foo.chunk.1.js', 'foo async bundle' ); expectFileMatchesSnapshotWithCompression('plugins/bar/target/public/bar.plugin.js', 'bar bundle'); diff --git a/packages/kbn-optimizer/src/report_optimizer_stats.ts b/packages/kbn-optimizer/src/report_optimizer_stats.ts index 5f3153bff5175..2f92f3d648ab7 100644 --- a/packages/kbn-optimizer/src/report_optimizer_stats.ts +++ b/packages/kbn-optimizer/src/report_optimizer_stats.ts @@ -17,6 +17,9 @@ * under the License. */ +import Fs from 'fs'; +import Path from 'path'; + import { materialize, mergeMap, dematerialize } from 'rxjs/operators'; import { CiStatsReporter } from '@kbn/dev-utils'; @@ -24,6 +27,32 @@ import { OptimizerUpdate$ } from './run_optimizer'; import { OptimizerState, OptimizerConfig } from './optimizer'; import { pipeClosure } from './common'; +const flatten = (arr: Array): T[] => + arr.reduce((acc: T[], item) => acc.concat(item), []); + +interface Entry { + relPath: string; + stats: Fs.Stats; +} + +const getFiles = (dir: string, parent?: string) => + flatten( + Fs.readdirSync(dir).map((name): Entry | Entry[] => { + const absPath = Path.join(dir, name); + const relPath = parent ? Path.join(parent, name) : name; + const stats = Fs.statSync(absPath); + + if (stats.isDirectory()) { + return getFiles(absPath, relPath); + } + + return { + relPath, + stats, + }; + }) + ); + export function reportOptimizerStats(reporter: CiStatsReporter, config: OptimizerConfig) { return pipeClosure((update$: OptimizerUpdate$) => { let lastState: OptimizerState | undefined; @@ -36,16 +65,55 @@ export function reportOptimizerStats(reporter: CiStatsReporter, config: Optimize if (n.kind === 'C' && lastState) { await reporter.metrics( - config.bundles.map((bundle) => { - // make the cache read from the cache file since it was likely updated by the worker - bundle.cache.refresh(); - - return { - group: `@kbn/optimizer bundle module count`, - id: bundle.id, - value: bundle.cache.getModuleCount() || 0, - }; - }) + flatten( + config.bundles.map((bundle) => { + // make the cache read from the cache file since it was likely updated by the worker + bundle.cache.refresh(); + + const outputFiles = getFiles(bundle.outputDir).filter( + (file) => !(file.relPath.startsWith('.') || file.relPath.endsWith('.map')) + ); + + const entryName = `${bundle.id}.${bundle.type}.js`; + const entry = outputFiles.find((f) => f.relPath === entryName); + if (!entry) { + throw new Error( + `Unable to find bundle entry named [${entryName}] in [${bundle.outputDir}]` + ); + } + + const chunkPrefix = `${bundle.id}.chunk.`; + const asyncChunks = outputFiles.filter((f) => f.relPath.startsWith(chunkPrefix)); + const miscFiles = outputFiles.filter( + (f) => f !== entry && !asyncChunks.includes(f) + ); + const sumSize = (files: Entry[]) => + files.reduce((acc: number, f) => acc + f.stats!.size, 0); + + return [ + { + group: `@kbn/optimizer bundle module count`, + id: bundle.id, + value: bundle.cache.getModuleCount() || 0, + }, + { + group: `page load bundle size`, + id: bundle.id, + value: entry.stats!.size, + }, + { + group: `async chunks size`, + id: bundle.id, + value: sumSize(asyncChunks), + }, + { + group: `miscellaneous assets size`, + id: bundle.id, + value: sumSize(miscFiles), + }, + ]; + }) + ) ); } diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index aaea70d12c60d..271ad49aee351 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -52,7 +52,8 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: output: { path: bundle.outputDir, - filename: `[name].${bundle.type}.js`, + filename: `${bundle.id}.${bundle.type}.js`, + chunkFilename: `${bundle.id}.chunk.[id].js`, devtoolModuleFilenameTemplate: (info) => `/${bundle.type}:${bundle.id}/${Path.relative( bundle.sourceRoot, diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json index 0c49ccf276b2b..38e4668fc1e42 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -16,7 +16,6 @@ "@types/joi": "^13.4.2", "@types/lodash": "^4.14.155", "@types/parse-link-header": "^1.0.0", - "@types/puppeteer": "^3.0.0", "@types/strip-ansi": "^5.2.1", "@types/xml2js": "^0.4.5", "diff": "^4.0.1" @@ -31,7 +30,6 @@ "joi": "^13.5.2", "lodash": "^4.17.15", "parse-link-header": "^1.0.1", - "puppeteer": "^3.3.0", "rxjs": "^6.5.5", "strip-ansi": "^5.2.0", "tar-fs": "^1.16.3", diff --git a/packages/kbn-test/src/index.ts b/packages/kbn-test/src/index.ts index 46f753b909553..f7321ca713087 100644 --- a/packages/kbn-test/src/index.ts +++ b/packages/kbn-test/src/index.ts @@ -60,4 +60,3 @@ export { makeJunitReportPath } from './junit_report_path'; export { CI_PARALLEL_PROCESS_PREFIX } from './ci_parallel_process_prefix'; export * from './functional_test_runner'; -export * from './page_load_metrics'; diff --git a/packages/kbn-test/src/page_load_metrics/capture_page_load_metrics.ts b/packages/kbn-test/src/page_load_metrics/capture_page_load_metrics.ts deleted file mode 100644 index 013d49a29a51c..0000000000000 --- a/packages/kbn-test/src/page_load_metrics/capture_page_load_metrics.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { ToolingLog } from '@kbn/dev-utils'; -import { NavigationOptions, createUrl, navigateToApps } from './navigation'; - -export async function capturePageLoadMetrics(log: ToolingLog, options: NavigationOptions) { - const responsesByPageView = await navigateToApps(log, options); - - const assetSizeMeasurements = new Map(); - - const numberOfPagesVisited = responsesByPageView.size; - - for (const [, frameResponses] of responsesByPageView) { - for (const [, { url, dataLength }] of frameResponses) { - if (url.length === 0) { - throw new Error('navigateToApps(); failed to identify the url of the request'); - } - if (assetSizeMeasurements.has(url)) { - assetSizeMeasurements.set(url, [dataLength].concat(assetSizeMeasurements.get(url) || [])); - } else { - assetSizeMeasurements.set(url, [dataLength]); - } - } - } - - return Array.from(assetSizeMeasurements.entries()) - .map(([url, measurements]) => { - const baseUrl = createUrl('/', options.appConfig.url); - const relativeUrl = url - // remove the baseUrl (expect the trailing slash) to make url relative - .replace(baseUrl.slice(0, -1), '') - // strip the build number from asset urls - .replace(/^\/\d+\//, '/'); - return [relativeUrl, measurements] as const; - }) - .filter(([url, measurements]) => { - if (measurements.length !== numberOfPagesVisited) { - // ignore urls seen only on some pages - return false; - } - - if (url.startsWith('data:')) { - // ignore data urls since they are already counted by other assets - return false; - } - - if (url.startsWith('/api/') || url.startsWith('/internal/')) { - // ignore api requests since they don't have deterministic sizes - return false; - } - - const allMetricsAreEqual = measurements.every((x, i) => - i === 0 ? true : x === measurements[i - 1] - ); - if (!allMetricsAreEqual) { - throw new Error(`measurements for url [${url}] are not equal [${measurements.join(',')}]`); - } - - return true; - }) - .map(([url, measurements]) => { - return { group: 'page load asset size', id: url, value: measurements[0] }; - }); -} diff --git a/packages/kbn-test/src/page_load_metrics/cli.ts b/packages/kbn-test/src/page_load_metrics/cli.ts deleted file mode 100644 index 95421384c79cb..0000000000000 --- a/packages/kbn-test/src/page_load_metrics/cli.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import Url from 'url'; - -import { run, createFlagError } from '@kbn/dev-utils'; -import { resolve, basename } from 'path'; -import { capturePageLoadMetrics } from './capture_page_load_metrics'; - -const defaultScreenshotsDir = resolve(__dirname, 'screenshots'); - -export function runPageLoadMetricsCli() { - run( - async ({ flags, log }) => { - const kibanaUrl = flags['kibana-url']; - if (!kibanaUrl || typeof kibanaUrl !== 'string') { - throw createFlagError('Expect --kibana-url to be a string'); - } - - const parsedUrl = Url.parse(kibanaUrl); - - const [username, password] = parsedUrl.auth - ? parsedUrl.auth.split(':') - : [flags.username, flags.password]; - - if (typeof username !== 'string' || typeof password !== 'string') { - throw createFlagError( - 'Mising username and/or password, either specify in --kibana-url or pass --username and --password' - ); - } - - const headless = !flags.head; - - const screenshotsDir = flags.screenshotsDir || defaultScreenshotsDir; - - if (typeof screenshotsDir !== 'string' || screenshotsDir === basename(screenshotsDir)) { - throw createFlagError('Expect screenshotsDir to be valid path string'); - } - - const metrics = await capturePageLoadMetrics(log, { - headless, - appConfig: { - url: kibanaUrl, - username, - password, - }, - screenshotsDir, - }); - for (const metric of metrics) { - log.info(`${metric.id}: ${metric.value}`); - } - }, - { - description: `Loads several pages with Puppeteer to capture the size of assets`, - flags: { - string: ['kibana-url', 'username', 'password', 'screenshotsDir'], - boolean: ['head'], - default: { - username: 'elastic', - password: 'changeme', - debug: true, - screenshotsDir: defaultScreenshotsDir, - }, - help: ` - --kibana-url Url for Kibana we should connect to, can include login info - --head Run puppeteer with graphical user interface - --username Set username, defaults to 'elastic' - --password Set password, defaults to 'changeme' - --screenshotsDir Set screenshots directory, defaults to '${defaultScreenshotsDir}' - `, - }, - } - ); -} diff --git a/packages/kbn-test/src/page_load_metrics/event.ts b/packages/kbn-test/src/page_load_metrics/event.ts deleted file mode 100644 index 481954bbf672e..0000000000000 --- a/packages/kbn-test/src/page_load_metrics/event.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export interface ResponseReceivedEvent { - frameId: string; - loaderId: string; - requestId: string; - response: Record; - timestamp: number; - type: string; -} - -export interface DataReceivedEvent { - encodedDataLength: number; - dataLength: number; - requestId: string; - timestamp: number; -} diff --git a/packages/kbn-test/src/page_load_metrics/index.ts b/packages/kbn-test/src/page_load_metrics/index.ts deleted file mode 100644 index 4309d558518a6..0000000000000 --- a/packages/kbn-test/src/page_load_metrics/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export * from './cli'; -export { capturePageLoadMetrics } from './capture_page_load_metrics'; diff --git a/packages/kbn-test/src/page_load_metrics/navigation.ts b/packages/kbn-test/src/page_load_metrics/navigation.ts deleted file mode 100644 index db53df789ac69..0000000000000 --- a/packages/kbn-test/src/page_load_metrics/navigation.ts +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import Fs from 'fs'; -import Url from 'url'; -import puppeteer from 'puppeteer'; -import { resolve } from 'path'; -import { ToolingLog } from '@kbn/dev-utils'; -import { ResponseReceivedEvent, DataReceivedEvent } from './event'; - -export interface NavigationOptions { - headless: boolean; - appConfig: { url: string; username: string; password: string }; - screenshotsDir: string; -} - -export type NavigationResults = Map>; - -interface FrameResponse { - url: string; - dataLength: number; -} - -function joinPath(pathA: string, pathB: string) { - return `${pathA.endsWith('/') ? pathA.slice(0, -1) : pathA}/${ - pathB.startsWith('/') ? pathB.slice(1) : pathB - }`; -} - -export function createUrl(path: string, url: string) { - const baseUrl = Url.parse(url); - return Url.format({ - protocol: baseUrl.protocol, - hostname: baseUrl.hostname, - port: baseUrl.port, - pathname: joinPath(baseUrl.pathname || '', path), - }); -} - -async function loginToKibana( - log: ToolingLog, - browser: puppeteer.Browser, - options: NavigationOptions -) { - log.debug(`log in to the app..`); - const page = await browser.newPage(); - const loginUrl = createUrl('/login', options.appConfig.url); - await page.goto(loginUrl, { - waitUntil: 'networkidle0', - }); - await page.type('[data-test-subj="loginUsername"]', options.appConfig.username); - await page.type('[data-test-subj="loginPassword"]', options.appConfig.password); - await page.click('[data-test-subj="loginSubmit"]'); - await page.waitForNavigation({ waitUntil: 'networkidle0' }); - await page.close(); -} - -export async function navigateToApps(log: ToolingLog, options: NavigationOptions) { - const browser = await puppeteer.launch({ headless: options.headless, args: ['--no-sandbox'] }); - const devToolsResponses: NavigationResults = new Map(); - const apps = [ - { path: '/app/discover', locator: '[data-test-subj="discover-sidebar"]' }, - { path: '/app/home', locator: '[data-test-subj="homeApp"]' }, - { path: '/app/canvas', locator: '[data-test-subj="create-workpad-button"]' }, - { path: '/app/maps', locator: '[title="Maps"]' }, - { path: '/app/apm', locator: '[data-test-subj="apmMainContainer"]' }, - ]; - - await loginToKibana(log, browser, options); - - await Promise.all( - apps.map(async (app) => { - const page = await browser.newPage(); - page.setCacheEnabled(false); - page.setDefaultNavigationTimeout(0); - const frameResponses = new Map(); - devToolsResponses.set(app.path, frameResponses); - - const client = await page.target().createCDPSession(); - await client.send('Network.enable'); - - function getRequestData(requestId: string) { - if (!frameResponses.has(requestId)) { - frameResponses.set(requestId, { url: '', dataLength: 0 }); - } - - return frameResponses.get(requestId)!; - } - - client.on('Network.responseReceived', (event: ResponseReceivedEvent) => { - getRequestData(event.requestId).url = event.response.url; - }); - - client.on('Network.dataReceived', (event: DataReceivedEvent) => { - getRequestData(event.requestId).dataLength += event.dataLength; - }); - - const url = createUrl(app.path, options.appConfig.url); - log.debug(`goto ${url}`); - await page.goto(url, { - waitUntil: 'networkidle0', - }); - - let readyAttempt = 0; - let selectorFound = false; - while (!selectorFound) { - readyAttempt += 1; - try { - await page.waitForSelector(app.locator, { timeout: 5000 }); - selectorFound = true; - } catch (error) { - log.error( - `Page '${app.path}' was not loaded properly, unable to find '${ - app.locator - }', url: ${page.url()}` - ); - - if (readyAttempt < 6) { - continue; - } - - const failureDir = resolve(options.screenshotsDir, 'failure'); - const screenshotPath = resolve( - failureDir, - `${app.path.slice(1).split('/').join('_')}_navigation.png` - ); - Fs.mkdirSync(failureDir, { recursive: true }); - - await page.bringToFront(); - await page.screenshot({ - path: screenshotPath, - type: 'png', - fullPage: true, - }); - log.debug(`Saving screenshot to ${screenshotPath}`); - - throw new Error(`Page load timeout: ${app.path} not loaded after 30 seconds`); - } - } - - await page.close(); - }) - ); - - await browser.close(); - - return devToolsResponses; -} diff --git a/scripts/page_load_metrics.js b/scripts/page_load_metrics.js deleted file mode 100644 index 37500c26e0b20..0000000000000 --- a/scripts/page_load_metrics.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -require('../src/setup_node_env'); -require('@kbn/test').runPageLoadMetricsCli(); diff --git a/test/scripts/jenkins_xpack_page_load_metrics.sh b/test/scripts/jenkins_xpack_page_load_metrics.sh deleted file mode 100644 index 679f0b8d2ddc5..0000000000000 --- a/test/scripts/jenkins_xpack_page_load_metrics.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -source test/scripts/jenkins_test_setup_xpack.sh - -checks-reporter-with-killswitch "Capture Kibana page load metrics" \ - node scripts/functional_tests \ - --debug --bail \ - --kibana-install-dir "$installDir" \ - --config test/page_load_metrics/config.ts; diff --git a/test/scripts/jenkins_xpack_visual_regression.sh b/test/scripts/jenkins_xpack_visual_regression.sh index 06a53277b8688..7fb7d7b71b2e4 100755 --- a/test/scripts/jenkins_xpack_visual_regression.sh +++ b/test/scripts/jenkins_xpack_visual_regression.sh @@ -17,6 +17,9 @@ tar -xzf "$linuxBuild" -C "$installDir" --strip=1 cd "$KIBANA_DIR" source "test/scripts/jenkins_xpack_saved_objects_field_metrics.sh" +cd "$KIBANA_DIR" +source "test/scripts/jenkins_xpack_saved_objects_field_metrics.sh" + echo " -> running visual regression tests from x-pack directory" cd "$XPACK_DIR" yarn percy exec -t 10000 -- -- \ diff --git a/x-pack/.gitignore b/x-pack/.gitignore index 0c916ef0e9b91..d73b6f64f036a 100644 --- a/x-pack/.gitignore +++ b/x-pack/.gitignore @@ -3,7 +3,6 @@ /target /test/functional/failure_debug /test/functional/screenshots -/test/page_load_metrics/screenshots /test/functional/apps/reporting/reports/session /test/reporting/configs/failure_debug/ /plugins/reporting/.chromium/ diff --git a/x-pack/test/page_load_metrics/config.ts b/x-pack/test/page_load_metrics/config.ts deleted file mode 100644 index 641099ff8e934..0000000000000 --- a/x-pack/test/page_load_metrics/config.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { resolve } from 'path'; - -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; -import { PuppeteerTestRunner } from './runner'; - -export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const kibanaCommonTestsConfig = await readConfigFile( - require.resolve('../../../test/common/config.js') - ); - const xpackFunctionalTestsConfig = await readConfigFile( - require.resolve('../functional/config.js') - ); - - return { - ...kibanaCommonTestsConfig.getAll(), - - testRunner: PuppeteerTestRunner, - - esArchiver: { - directory: resolve(__dirname, 'es_archives'), - }, - - screenshots: { - directory: resolve(__dirname, 'screenshots'), - }, - - esTestCluster: { - ...xpackFunctionalTestsConfig.get('esTestCluster'), - serverArgs: [...xpackFunctionalTestsConfig.get('esTestCluster.serverArgs')], - }, - - kbnTestServer: { - ...xpackFunctionalTestsConfig.get('kbnTestServer'), - }, - }; -} diff --git a/x-pack/test/page_load_metrics/es_archives/default/data.json.gz b/x-pack/test/page_load_metrics/es_archives/default/data.json.gz deleted file mode 100644 index 5a5290ddf64478d0dfd175e7b91ad91efa5c61ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1812 zcmV+v2kZDBiwFP!000026YW~vQ`ODC%jnK86-Mz#En8LuAk1&$c@Sdr*$gngb#vtbu z4|0y!F`|bstBi{A2y%F$II=yGr+i^ta+r(+5$sl}3B0bO;#5*g#M|-=9VP4xg`Ch& zaEfTH#Oe#NoOdeR+H~#{S+P?ivcTwAco@-?ea3wJ9+7>F;%Ke|*f9B+;FNFm#>p6F zXyqF+5Kak)aor$8oa1!F79)U-$(0CoIcc5@ z5Iq|133-c^FUmJ5{n6&PuKza_q%H#s9K&9AAPEOb9g00;(f4#&pph%vS)ujNZIod5BWn)66 zu-^d~3f{+FKE`=rbC8#QcqIxn0ZAUVZ#){R2-Gxcvcs5dj_z&j-j2vA*6WrR+}hmxUBxKYY*?~Q!IEcl~gLGCL&(eXIiJ* z7w4o>rd?Y=d%s?yRZw-Z;?NP%oWBFGgfRY-^OQdc@l|9W$-64trypX&jR z&h_1MWrZRzu~^cPq9La4X$AS~Y~qEWs-+`nK>RL}DiR~Uy2_O#1Zg;yX;TnYhCbKf zXhhKn@+y@g810K>@s5ON(q(MU<#xBKp){(glEvH~q9+RpMO8hE$XB#&)R_|(^qG?z zE2QG1s!^F(b=`d6;>RdkXxIqkV#bfact}Vy9XYz@kEP<4o)kKEFVXah(1t7fZt|0R zbT-8D!D*qC&^rcDhbZ2!dS;O0IQlzJ1ho%}UT#*T5&g zjtfLUG7=;I_~_)cs4{*%+ewB}DKN`^{%_Msse`8S)b8w+tn8nWXYp#!M7a$jgfzn9 zpn~~yv9F?ZkXdd=nO#U!sZv}V2Q?z^#*Jv9Gi9j_rpV%IvuOPO&Z?(sgU$Wr#Iiwz z;t^gvr#@4o>XDSeywn_{)DDW>E-#9gn%<%~Uf=aZsk-=Dc&$)-cROAQybctXKc(yjH$Qj8}|6FTDGMGLRD*<2| zi`J&6ri#?Aq%mXxOs7G~*~RgsQtQ{iFq5#L3zrRTb!kOx5i1hPd}G?2Qy3={)iYuE zI}(*dFqm?ssR_K6d6SJr5VO^m84WrOIhviFmYkNvwHYb+Mg}aX^F+OfyZ*LpMUof( z<|*QffuWDc66qV9z-1!q3?M^G^pr&C8(Si0Q$ALqStZkaFs&gboq|PuuOeiZ?J_$j z9(Q7I~?F%)%qZ@4i4F$~dLBKfN+EJF(Sjfsn zv`L29YbYb5MA*T^_KYSiKsOxIY?@S7Ze?pF*iA8?6n8Y+@^_n*UG2XXBwM!xTfQM% zhU|xYgf-rFzI-_NtN(HTpEG>wf$KAS>6jt!yjGtuhD2sbeE?}ijsE~&R^>A`FaQ8s CuYsii diff --git a/x-pack/test/page_load_metrics/es_archives/default/mappings.json b/x-pack/test/page_load_metrics/es_archives/default/mappings.json deleted file mode 100644 index c36f9576c4df1..0000000000000 --- a/x-pack/test/page_load_metrics/es_archives/default/mappings.json +++ /dev/null @@ -1,2402 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - ".kibana": { - } - }, - "index": ".kibana_1", - "mappings": { - "_meta": { - "migrationMappingPropertyHashes": { - "action": "6e96ac5e648f57523879661ea72525b7", - "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", - "alert": "7b44fba6773e37c806ce290ea9b7024e", - "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", - "apm-telemetry": "3525d7c22c42bc80f5e6e9cb3f2b26a2", - "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", - "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", - "canvas-element": "7390014e1091044523666d97247392fc", - "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", - "cases": "32aa96a6d3855ddda53010ae2048ac22", - "cases-comments": "c2061fb929f585df57425102fa928b4b", - "cases-configure": "42711cbb311976c0687853f4c1354572", - "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", - "config": "ae24d22d5986d04124cc6568f771066f", - "dashboard": "d00f614b29a80360e1190193fd333bab", - "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", - "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", - "index-pattern": "66eccb05066c5a89924f48a9e9736499", - "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", - "inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2", - "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", - "lens": "21c3ea0763beb1ecb0162529706b88c5", - "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", - "map": "23d7aa4a720d4938ccde3983f87bd58d", - "maps-telemetry": "bfd39d88aadadb4be597ea984d433dbe", - "metrics-explorer-view": "428e319af3e822c80a84cf87123ca35c", - "migrationVersion": "4a1746014a75ade3a714e1db5763276f", - "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", - "namespace": "2f4316de49999235636386fe51dc06c1", - "namespaces": "2f4316de49999235636386fe51dc06c1", - "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", - "references": "7997cf5a56cc02bdc9c93361bde732b0", - "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", - "search": "181661168bbadd1eff5902361e2a0d5c", - "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", - "telemetry": "36a616f7026dfa617d6655df850fe16d", - "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", - "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", - "type": "2f4316de49999235636386fe51dc06c1", - "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", - "updated_at": "00da57df13e94e9d98437d13ace4bfe0", - "upgrade-assistant-reindex-operation": "296a89039fc4260292be36b1b005d8f2", - "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", - "uptime-dynamic-settings": "fcdb453a30092f022f2642db29523d80", - "url": "b675c3be8d76ecf029294d51dc7ec65d", - "visualization": "52d7a13ad68a150c4525b292d23e12cc" - } - }, - "dynamic": "strict", - "properties": { - "action": { - "properties": { - "actionTypeId": { - "type": "keyword" - }, - "config": { - "enabled": false, - "type": "object" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "secrets": { - "type": "binary" - } - } - }, - "action_task_params": { - "properties": { - "actionId": { - "type": "keyword" - }, - "apiKey": { - "type": "binary" - }, - "params": { - "enabled": false, - "type": "object" - } - } - }, - "alert": { - "properties": { - "actions": { - "properties": { - "actionRef": { - "type": "keyword" - }, - "actionTypeId": { - "type": "keyword" - }, - "group": { - "type": "keyword" - }, - "params": { - "enabled": false, - "type": "object" - } - }, - "type": "nested" - }, - "alertTypeId": { - "type": "keyword" - }, - "apiKey": { - "type": "binary" - }, - "apiKeyOwner": { - "type": "keyword" - }, - "consumer": { - "type": "keyword" - }, - "createdAt": { - "type": "date" - }, - "createdBy": { - "type": "keyword" - }, - "enabled": { - "type": "boolean" - }, - "muteAll": { - "type": "boolean" - }, - "mutedInstanceIds": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "params": { - "enabled": false, - "type": "object" - }, - "schedule": { - "properties": { - "interval": { - "type": "keyword" - } - } - }, - "scheduledTaskId": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "throttle": { - "type": "keyword" - }, - "updatedBy": { - "type": "keyword" - } - } - }, - "apm-indices": { - "properties": { - "apm_oss": { - "properties": { - "errorIndices": { - "type": "keyword" - }, - "metricsIndices": { - "type": "keyword" - }, - "onboardingIndices": { - "type": "keyword" - }, - "sourcemapIndices": { - "type": "keyword" - }, - "spanIndices": { - "type": "keyword" - }, - "transactionIndices": { - "type": "keyword" - } - } - } - } - }, - "apm-telemetry": { - "properties": { - "agents": { - "properties": { - "dotnet": { - "properties": { - "agent": { - "properties": { - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "language": { - "properties": { - "composite": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "composite": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - } - } - }, - "go": { - "properties": { - "agent": { - "properties": { - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "language": { - "properties": { - "composite": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "composite": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - } - } - }, - "java": { - "properties": { - "agent": { - "properties": { - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "language": { - "properties": { - "composite": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "composite": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - } - } - }, - "js-base": { - "properties": { - "agent": { - "properties": { - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "language": { - "properties": { - "composite": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "composite": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - } - } - }, - "nodejs": { - "properties": { - "agent": { - "properties": { - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "language": { - "properties": { - "composite": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "composite": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - } - } - }, - "python": { - "properties": { - "agent": { - "properties": { - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "language": { - "properties": { - "composite": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "composite": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - } - } - }, - "ruby": { - "properties": { - "agent": { - "properties": { - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "language": { - "properties": { - "composite": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "composite": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - } - } - }, - "rum-js": { - "properties": { - "agent": { - "properties": { - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "language": { - "properties": { - "composite": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "composite": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - } - } - } - } - }, - "cardinality": { - "properties": { - "transaction": { - "properties": { - "name": { - "properties": { - "all_agents": { - "properties": { - "1d": { - "type": "long" - } - } - }, - "rum": { - "properties": { - "1d": { - "type": "long" - } - } - } - } - } - } - }, - "user_agent": { - "properties": { - "original": { - "properties": { - "all_agents": { - "properties": { - "1d": { - "type": "long" - } - } - }, - "rum": { - "properties": { - "1d": { - "type": "long" - } - } - } - } - } - } - } - } - }, - "counts": { - "properties": { - "agent_configuration": { - "properties": { - "all": { - "type": "long" - } - } - }, - "error": { - "properties": { - "1d": { - "type": "long" - }, - "all": { - "type": "long" - } - } - }, - "max_error_groups_per_service": { - "properties": { - "1d": { - "type": "long" - } - } - }, - "max_transaction_groups_per_service": { - "properties": { - "1d": { - "type": "long" - } - } - }, - "metric": { - "properties": { - "1d": { - "type": "long" - }, - "all": { - "type": "long" - } - } - }, - "onboarding": { - "properties": { - "1d": { - "type": "long" - }, - "all": { - "type": "long" - } - } - }, - "services": { - "properties": { - "1d": { - "type": "long" - } - } - }, - "sourcemap": { - "properties": { - "1d": { - "type": "long" - }, - "all": { - "type": "long" - } - } - }, - "span": { - "properties": { - "1d": { - "type": "long" - }, - "all": { - "type": "long" - } - } - }, - "traces": { - "properties": { - "1d": { - "type": "long" - } - } - }, - "transaction": { - "properties": { - "1d": { - "type": "long" - }, - "all": { - "type": "long" - } - } - } - } - }, - "has_any_services": { - "type": "boolean" - }, - "indices": { - "properties": { - "all": { - "properties": { - "total": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } - } - } - } - } - }, - "shards": { - "properties": { - "total": { - "type": "long" - } - } - } - } - }, - "integrations": { - "properties": { - "ml": { - "properties": { - "all_jobs_count": { - "type": "long" - } - } - } - } - }, - "retainment": { - "properties": { - "error": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "metric": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "onboarding": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "span": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "transaction": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "services_per_agent": { - "properties": { - "dotnet": { - "null_value": 0, - "type": "long" - }, - "go": { - "null_value": 0, - "type": "long" - }, - "java": { - "null_value": 0, - "type": "long" - }, - "js-base": { - "null_value": 0, - "type": "long" - }, - "nodejs": { - "null_value": 0, - "type": "long" - }, - "python": { - "null_value": 0, - "type": "long" - }, - "ruby": { - "null_value": 0, - "type": "long" - }, - "rum-js": { - "null_value": 0, - "type": "long" - } - } - }, - "tasks": { - "properties": { - "agent_configuration": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "agents": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "cardinality": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "groupings": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "indices_stats": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "integrations": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "processor_events": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "services": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "versions": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - } - } - }, - "version": { - "properties": { - "apm_server": { - "properties": { - "major": { - "type": "long" - }, - "minor": { - "type": "long" - }, - "patch": { - "type": "long" - } - } - } - } - } - } - }, - "application_usage_totals": { - "properties": { - "appId": { - "type": "keyword" - }, - "minutesOnScreen": { - "type": "float" - }, - "numberOfClicks": { - "type": "long" - } - } - }, - "application_usage_transactional": { - "properties": { - "appId": { - "type": "keyword" - }, - "minutesOnScreen": { - "type": "float" - }, - "numberOfClicks": { - "type": "long" - }, - "timestamp": { - "type": "date" - } - } - }, - "canvas-element": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "content": { - "type": "text" - }, - "help": { - "type": "text" - }, - "image": { - "type": "text" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "canvas-workpad": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "cases": { - "properties": { - "closed_at": { - "type": "date" - }, - "closed_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "connector_id": { - "type": "keyword" - }, - "created_at": { - "type": "date" - }, - "created_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "description": { - "type": "text" - }, - "external_service": { - "properties": { - "connector_id": { - "type": "keyword" - }, - "connector_name": { - "type": "keyword" - }, - "external_id": { - "type": "keyword" - }, - "external_title": { - "type": "text" - }, - "external_url": { - "type": "text" - }, - "pushed_at": { - "type": "date" - }, - "pushed_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - } - } - }, - "status": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "title": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - } - } - }, - "cases-comments": { - "properties": { - "comment": { - "type": "text" - }, - "created_at": { - "type": "date" - }, - "created_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "pushed_at": { - "type": "date" - }, - "pushed_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - } - } - }, - "cases-configure": { - "properties": { - "closure_type": { - "type": "keyword" - }, - "connector_id": { - "type": "keyword" - }, - "connector_name": { - "type": "keyword" - }, - "created_at": { - "type": "date" - }, - "created_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - } - } - }, - "cases-user-actions": { - "properties": { - "action": { - "type": "keyword" - }, - "action_at": { - "type": "date" - }, - "action_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "action_field": { - "type": "keyword" - }, - "new_value": { - "type": "text" - }, - "old_value": { - "type": "text" - } - } - }, - "config": { - "dynamic": "true", - "properties": { - "buildNum": { - "type": "keyword" - }, - "defaultIndex": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "file-upload-telemetry": { - "properties": { - "filesUploadedTotalCount": { - "type": "long" - } - } - }, - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "typeMeta": { - "type": "keyword" - } - } - }, - "infrastructure-ui-source": { - "properties": { - "description": { - "type": "text" - }, - "fields": { - "properties": { - "container": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "pod": { - "type": "keyword" - }, - "tiebreaker": { - "type": "keyword" - }, - "timestamp": { - "type": "keyword" - } - } - }, - "logAlias": { - "type": "keyword" - }, - "logColumns": { - "properties": { - "fieldColumn": { - "properties": { - "field": { - "type": "keyword" - }, - "id": { - "type": "keyword" - } - } - }, - "messageColumn": { - "properties": { - "id": { - "type": "keyword" - } - } - }, - "timestampColumn": { - "properties": { - "id": { - "type": "keyword" - } - } - } - }, - "type": "nested" - }, - "metricAlias": { - "type": "keyword" - }, - "name": { - "type": "text" - } - } - }, - "inventory-view": { - "properties": { - "accountId": { - "type": "keyword" - }, - "autoBounds": { - "type": "boolean" - }, - "autoReload": { - "type": "boolean" - }, - "boundsOverride": { - "properties": { - "max": { - "type": "integer" - }, - "min": { - "type": "integer" - } - } - }, - "customMetrics": { - "properties": { - "aggregation": { - "type": "keyword" - }, - "field": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "label": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "customOptions": { - "properties": { - "field": { - "type": "keyword" - }, - "text": { - "type": "keyword" - } - }, - "type": "nested" - }, - "filterQuery": { - "properties": { - "expression": { - "type": "keyword" - }, - "kind": { - "type": "keyword" - } - } - }, - "groupBy": { - "properties": { - "field": { - "type": "keyword" - }, - "label": { - "type": "keyword" - } - }, - "type": "nested" - }, - "legend": { - "properties": { - "palette": { - "type": "keyword" - }, - "reverseColors": { - "type": "boolean" - }, - "steps": { - "type": "long" - } - } - }, - "metric": { - "properties": { - "aggregation": { - "type": "keyword" - }, - "field": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "label": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "name": { - "type": "keyword" - }, - "nodeType": { - "type": "keyword" - }, - "region": { - "type": "keyword" - }, - "sort": { - "properties": { - "by": { - "type": "keyword" - }, - "direction": { - "type": "keyword" - } - } - }, - "time": { - "type": "long" - }, - "view": { - "type": "keyword" - } - } - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "lens": { - "properties": { - "expression": { - "index": false, - "type": "keyword" - }, - "state": { - "type": "flattened" - }, - "title": { - "type": "text" - }, - "visualizationType": { - "type": "keyword" - } - } - }, - "lens-ui-telemetry": { - "properties": { - "count": { - "type": "integer" - }, - "date": { - "type": "date" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "map": { - "properties": { - "bounds": { - "type": "geo_shape" - }, - "description": { - "type": "text" - }, - "layerListJSON": { - "type": "text" - }, - "mapStateJSON": { - "type": "text" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "maps-telemetry": { - "properties": { - "attributesPerMap": { - "properties": { - "dataSourcesCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - }, - "emsVectorLayersCount": { - "dynamic": "true", - "type": "object" - }, - "layerTypesCount": { - "dynamic": "true", - "type": "object" - }, - "layersCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - } - } - }, - "indexPatternsWithGeoFieldCount": { - "type": "long" - }, - "indexPatternsWithGeoPointFieldCount": { - "type": "long" - }, - "indexPatternsWithGeoShapeFieldCount": { - "type": "long" - }, - "mapsTotalCount": { - "type": "long" - }, - "settings": { - "properties": { - "showMapVisualizationTypes": { - "type": "boolean" - } - } - }, - "timeCaptured": { - "type": "date" - } - } - }, - "metrics-explorer-view": { - "properties": { - "chartOptions": { - "properties": { - "stack": { - "type": "boolean" - }, - "type": { - "type": "keyword" - }, - "yAxisMode": { - "type": "keyword" - } - } - }, - "currentTimerange": { - "properties": { - "from": { - "type": "keyword" - }, - "interval": { - "type": "keyword" - }, - "to": { - "type": "keyword" - } - } - }, - "name": { - "type": "keyword" - }, - "options": { - "properties": { - "aggregation": { - "type": "keyword" - }, - "filterQuery": { - "type": "keyword" - }, - "forceInterval": { - "type": "boolean" - }, - "groupBy": { - "type": "keyword" - }, - "limit": { - "type": "integer" - }, - "metrics": { - "properties": { - "aggregation": { - "type": "keyword" - }, - "color": { - "type": "keyword" - }, - "field": { - "type": "keyword" - }, - "label": { - "type": "keyword" - } - }, - "type": "nested" - } - } - } - } - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "index-pattern": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "space": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "ml-telemetry": { - "properties": { - "file_data_visualizer": { - "properties": { - "index_creation_count": { - "type": "long" - } - } - } - } - }, - "namespace": { - "type": "keyword" - }, - "namespaces": { - "type": "keyword" - }, - "query": { - "properties": { - "description": { - "type": "text" - }, - "filters": { - "enabled": false, - "type": "object" - }, - "query": { - "properties": { - "language": { - "type": "keyword" - }, - "query": { - "index": false, - "type": "keyword" - } - } - }, - "timefilter": { - "enabled": false, - "type": "object" - }, - "title": { - "type": "text" - } - } - }, - "references": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "sample-data-telemetry": { - "properties": { - "installCount": { - "type": "long" - }, - "unInstallCount": { - "type": "long" - } - } - }, - "search": { - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "imageUrl": { - "index": false, - "type": "text" - }, - "initials": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "telemetry": { - "properties": { - "allowChangingOptInStatus": { - "type": "boolean" - }, - "enabled": { - "type": "boolean" - }, - "lastReported": { - "type": "date" - }, - "lastVersionChecked": { - "type": "keyword" - }, - "reportFailureCount": { - "type": "integer" - }, - "reportFailureVersion": { - "type": "keyword" - }, - "sendUsageFrom": { - "type": "keyword" - }, - "userHasSeenNotice": { - "type": "boolean" - } - } - }, - "timelion-sheet": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "timelion_chart_height": { - "type": "integer" - }, - "timelion_columns": { - "type": "integer" - }, - "timelion_interval": { - "type": "keyword" - }, - "timelion_other_interval": { - "type": "keyword" - }, - "timelion_rows": { - "type": "integer" - }, - "timelion_sheet": { - "type": "text" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "tsvb-validation-telemetry": { - "properties": { - "failedRequests": { - "type": "long" - } - } - }, - "type": { - "type": "keyword" - }, - "ui-metric": { - "properties": { - "count": { - "type": "integer" - } - } - }, - "updated_at": { - "type": "date" - }, - "upgrade-assistant-reindex-operation": { - "properties": { - "errorMessage": { - "type": "keyword" - }, - "indexName": { - "type": "keyword" - }, - "lastCompletedStep": { - "type": "integer" - }, - "locked": { - "type": "date" - }, - "newIndexName": { - "type": "keyword" - }, - "reindexOptions": { - "properties": { - "openAndClose": { - "type": "boolean" - }, - "queueSettings": { - "properties": { - "queuedAt": { - "type": "long" - }, - "startedAt": { - "type": "long" - } - } - } - } - }, - "reindexTaskId": { - "type": "keyword" - }, - "reindexTaskPercComplete": { - "type": "float" - }, - "runningReindexCount": { - "type": "integer" - }, - "status": { - "type": "integer" - } - } - }, - "upgrade-assistant-telemetry": { - "properties": { - "features": { - "properties": { - "deprecation_logging": { - "properties": { - "enabled": { - "null_value": true, - "type": "boolean" - } - } - } - } - }, - "ui_open": { - "properties": { - "cluster": { - "null_value": 0, - "type": "long" - }, - "indices": { - "null_value": 0, - "type": "long" - }, - "overview": { - "null_value": 0, - "type": "long" - } - } - }, - "ui_reindex": { - "properties": { - "close": { - "null_value": 0, - "type": "long" - }, - "open": { - "null_value": 0, - "type": "long" - }, - "start": { - "null_value": 0, - "type": "long" - }, - "stop": { - "null_value": 0, - "type": "long" - } - } - } - } - }, - "uptime-dynamic-settings": { - "properties": { - "certAgeThreshold": { - "type": "long" - }, - "certExpirationThreshold": { - "type": "long" - }, - "heartbeatIndices": { - "type": "keyword" - } - } - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchRefName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - }, - "settings": { - "index": { - "auto_expand_replicas": "0-1", - "number_of_replicas": "0", - "number_of_shards": "1" - } - } - } -} - -{ - "type": "index", - "value": { - "aliases": { - }, - "index": "test", - "mappings": { - "properties": { - "foo": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "settings": { - "index": { - "number_of_replicas": "1", - "number_of_shards": "1" - } - } - } -} \ No newline at end of file diff --git a/x-pack/test/page_load_metrics/runner.ts b/x-pack/test/page_load_metrics/runner.ts deleted file mode 100644 index 05f293730f843..0000000000000 --- a/x-pack/test/page_load_metrics/runner.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { CiStatsReporter } from '@kbn/dev-utils'; -import { capturePageLoadMetrics } from '@kbn/test'; -// @ts-ignore not TS yet -import getUrl from '../../../src/test_utils/get_url'; - -import { FtrProviderContext } from './../functional/ftr_provider_context'; - -export async function PuppeteerTestRunner({ getService }: FtrProviderContext) { - const log = getService('log'); - const config = getService('config'); - const esArchiver = getService('esArchiver'); - - await esArchiver.load('default'); - const metrics = await capturePageLoadMetrics(log, { - headless: true, - appConfig: { - url: getUrl.baseUrl(config.get('servers.kibana')), - username: config.get('servers.kibana.username'), - password: config.get('servers.kibana.password'), - }, - screenshotsDir: config.get('screenshots.directory'), - }); - const reporter = CiStatsReporter.fromEnv(log); - - log.debug('Report page load asset size'); - await reporter.metrics(metrics); -} diff --git a/yarn.lock b/yarn.lock index bd6c2031d0ec8..b8aa559bc1d40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5619,13 +5619,6 @@ dependencies: "@types/node" "*" -"@types/puppeteer@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-3.0.0.tgz#24cdcc131e319477608d893f0017e08befd70423" - integrity sha512-59+fkfHHXHzX5rgoXIMnZyzum7ZLx/Wc3fhsOduFThpTpKbzzdBHMZsrkKGLunimB4Ds/tI5lXTRLALK8Mmnhg== - dependencies: - "@types/node" "*" - "@types/q@^1.5.1": version "1.5.2" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" @@ -8700,15 +8693,6 @@ bl@^3.0.0: dependencies: readable-stream "^3.0.1" -bl@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.2.tgz#52b71e9088515d0606d9dd9cc7aa48dc1f98e73a" - integrity sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ== - dependencies: - buffer "^5.5.0" - inherits "^2.0.4" - readable-stream "^3.4.0" - blob@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" @@ -9215,14 +9199,6 @@ buffer@^5.1.0, buffer@^5.2.0: base64-js "^1.0.2" ieee754 "^1.1.4" -buffer@^5.2.1, buffer@^5.5.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" - integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - builtin-modules@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -17675,7 +17651,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4: +inherits@2, inherits@2.0.4, inherits@~2.0.3, inherits@~2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -21893,11 +21869,6 @@ mixin-object@^2.0.1: for-in "^0.1.3" is-extendable "^0.1.1" -mkdirp-classic@^0.5.2: - version "0.5.3" - resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" - integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== - mkdirp@0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" @@ -25075,22 +25046,6 @@ puppeteer@^2.0.0: rimraf "^2.6.1" ws "^6.1.0" -puppeteer@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-3.3.0.tgz#95839af9fdc0aa4de7e5ee073a4c0adeb9e2d3d7" - integrity sha512-23zNqRltZ1PPoK28uRefWJ/zKb5Jhnzbbwbpcna2o5+QMn17F0khq5s1bdH3vPlyj+J36pubccR8wiNA/VE0Vw== - dependencies: - debug "^4.1.0" - extract-zip "^2.0.0" - https-proxy-agent "^4.0.0" - mime "^2.0.3" - progress "^2.0.1" - proxy-from-env "^1.0.0" - rimraf "^3.0.2" - tar-fs "^2.0.0" - unbzip2-stream "^1.3.3" - ws "^7.2.3" - q@^1.1.2: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" @@ -29745,16 +29700,6 @@ tar-fs@^1.16.3: pump "^1.0.0" tar-stream "^1.1.2" -tar-fs@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.0.tgz#d1cdd121ab465ee0eb9ccde2d35049d3f3daf0d5" - integrity sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg== - dependencies: - chownr "^1.1.1" - mkdirp-classic "^0.5.2" - pump "^3.0.0" - tar-stream "^2.0.0" - tar-stream@^1.1.2, tar-stream@^1.5.2: version "1.5.5" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.5.5.tgz#5cad84779f45c83b1f2508d96b09d88c7218af55" @@ -29765,17 +29710,6 @@ tar-stream@^1.1.2, tar-stream@^1.5.2: readable-stream "^2.0.0" xtend "^4.0.0" -tar-stream@^2.0.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.2.tgz#6d5ef1a7e5783a95ff70b69b97455a5968dc1325" - integrity sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q== - dependencies: - bl "^4.0.1" - end-of-stream "^1.4.1" - fs-constants "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.1.1" - tar-stream@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.0.tgz#d1aaa3661f05b38b5acc9b7020efdca5179a2cc3" @@ -30061,7 +29995,7 @@ through2@~2.0.3: readable-stream "~2.3.6" xtend "~4.0.1" -through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@^2.3.8, through@~2.3.4, through@~2.3.6, through@~2.3.8: +through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@~2.3.4, through@~2.3.6, through@~2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -31257,14 +31191,6 @@ unbzip2-stream@^1.0.9: buffer "^3.0.1" through "^2.3.6" -unbzip2-stream@^1.3.3: - version "1.4.2" - resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.2.tgz#84eb9e783b186d8fb397515fbb656f312f1a7dbf" - integrity sha512-pZMVAofMrrHX6Ik39hCk470kulCbmZ2SWfQLPmTWqfJV/oUm0gn1CblvHdUu4+54Je6Jq34x8kY6XjTy6dMkOg== - dependencies: - buffer "^5.2.1" - through "^2.3.8" - unc-path-regex@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" @@ -33215,11 +33141,6 @@ ws@^7.0.0: dependencies: async-limiter "^1.0.0" -ws@^7.2.3: - version "7.3.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd" - integrity sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w== - ws@~3.3.1: version "3.3.3" resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2" From e010ed3d09c82ccb3d15e76065ede0cd45a020b7 Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Wed, 15 Jul 2020 01:36:06 +0100 Subject: [PATCH 62/82] [ML] Edits labelling of SIEM module and jobs from SIEM to Security (#71696) ## Summary Edits all references to 'SIEM' in the ML SIEM modules to 'Security'. The following parts of the configurations were edited: - Module titles - Module descriptions - Job descriptions - `siem` job group changed to `security` The `siem#/` portion of the custom URLs was also edited to `security/`. Also removes the 'beta' label from module and job descriptions. ![image](https://user-images.githubusercontent.com/7405507/87452224-dbe4fd00-c5f8-11ea-887b-89c47e3467d2.png) ![image (26)](https://user-images.githubusercontent.com/7405507/87452265-edc6a000-c5f8-11ea-94a8-e101126666fa.png) Part of #69319 --- .../modules/siem_auditbeat/manifest.json | 4 +- .../linux_anomalous_network_activity_ecs.json | 12 +- ...x_anomalous_network_port_activity_ecs.json | 12 +- .../ml/linux_anomalous_network_service.json | 14 +- ...ux_anomalous_network_url_activity_ecs.json | 74 +++++------ ...linux_anomalous_process_all_hosts_ecs.json | 14 +- .../ml/linux_anomalous_user_name_ecs.json | 12 +- .../ml/rare_process_by_host_linux_ecs.json | 14 +- .../modules/siem_auditbeat_auth/manifest.json | 4 +- .../ml/suspicious_login_activity_ecs.json | 8 +- .../modules/siem_cloudtrail/manifest.json | 124 +++++++++--------- .../ml/high_distinct_count_error_message.json | 62 ++++----- .../siem_cloudtrail/ml/rare_error_code.json | 62 ++++----- .../ml/rare_method_for_a_city.json | 64 ++++----- .../ml/rare_method_for_a_country.json | 64 ++++----- .../ml/rare_method_for_a_username.json | 64 ++++----- .../modules/siem_packetbeat/manifest.json | 4 +- .../ml/packetbeat_dns_tunneling.json | 6 +- .../ml/packetbeat_rare_dns_question.json | 6 +- .../ml/packetbeat_rare_server_domain.json | 6 +- .../ml/packetbeat_rare_urls.json | 6 +- .../ml/packetbeat_rare_user_agent.json | 8 +- .../modules/siem_winlogbeat/manifest.json | 4 +- .../ml/rare_process_by_host_windows_ecs.json | 14 +- ...indows_anomalous_network_activity_ecs.json | 12 +- .../windows_anomalous_path_activity_ecs.json | 14 +- ...ndows_anomalous_process_all_hosts_ecs.json | 12 +- .../windows_anomalous_process_creation.json | 14 +- .../ml/windows_anomalous_script.json | 10 +- .../ml/windows_anomalous_service.json | 10 +- .../ml/windows_anomalous_user_name_ecs.json | 12 +- .../ml/windows_rare_user_runas_event.json | 12 +- .../siem_winlogbeat_auth/manifest.json | 4 +- ...windows_rare_user_type10_remote_login.json | 12 +- 34 files changed, 387 insertions(+), 387 deletions(-) diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/manifest.json index 3c7b1c7cfffd4..1e7fcdd4320f8 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/manifest.json @@ -1,7 +1,7 @@ { "id": "siem_auditbeat", - "title": "SIEM Auditbeat", - "description": "Detect suspicious network activity and unusual processes in Auditbeat data (beta).", + "title": "Security: Auditbeat", + "description": "Detect suspicious network activity and unusual processes in Auditbeat data.", "type": "Auditbeat data", "logoFile": "logo.json", "defaultIndexPattern": "auditbeat-*", diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_activity_ecs.json index e409903a2801e..eab14d7c11ba1 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_activity_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_activity_ecs.json @@ -1,8 +1,8 @@ { "job_type": "anomaly_detector", - "description": "SIEM Auditbeat: Looks for unusual processes using the network which could indicate command-and-control, lateral movement, persistence, or data exfiltration activity (beta)", + "description": "Security: Auditbeat - Looks for unusual processes using the network which could indicate command-and-control, lateral movement, persistence, or data exfiltration activity.", "groups": [ - "siem", + "security", "auditbeat", "process" ], @@ -34,19 +34,19 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/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_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": "siem#/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_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": "siem#/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_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": "siem#/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$')))" + "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$')))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_port_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_port_activity_ecs.json index a87c99da478d2..1891be831837b 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_port_activity_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_port_activity_ecs.json @@ -1,8 +1,8 @@ { "job_type": "anomaly_detector", - "description": "SIEM Auditbeat: Looks for unusual destination port activity that could indicate command-and-control, persistence mechanism, or data exfiltration activity (beta)", + "description": "Security: Auditbeat - Looks for unusual destination port activity that could indicate command-and-control, persistence mechanism, or data exfiltration activity.", "groups": [ - "siem", + "security", "auditbeat", "network" ], @@ -34,19 +34,19 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/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_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": "siem#/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_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": "siem#/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_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": "siem#/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$')))" + "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$')))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_service.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_service.json index 9ded51f09200b..8fd24dd817c35 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_service.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_service.json @@ -1,11 +1,11 @@ { "job_type": "anomaly_detector", "groups": [ - "siem", + "security", "auditbeat", "network" ], - "description": "SIEM Auditbeat: Looks for unusual listening ports that could indicate execution of unauthorized services, backdoors, or persistence mechanisms (beta)", + "description": "Security: Auditbeat - Looks for unusual listening ports that could indicate execution of unauthorized services, backdoors, or persistence mechanisms.", "analysis_config": { "bucket_span": "15m", "detectors": [ @@ -33,20 +33,20 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/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_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": "siem#/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_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": "siem#/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_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": "siem#/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$')))" + "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$')))" } ] } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_url_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_url_activity_ecs.json index 4f8da6c486fff..aa43a50e76863 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_url_activity_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_url_activity_ecs.json @@ -1,40 +1,40 @@ { - "job_type": "anomaly_detector", - "groups": [ - "siem", - "auditbeat", - "network" + "job_type": "anomaly_detector", + "groups": [ + "security", + "auditbeat", + "network" + ], + "description": "Security: Auditbeat - Looks for an unusual web URL request from a Linux instance. Curl and wget web request activity is very common but unusual web requests from a Linux server can sometimes be malware delivery or execution.", + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "rare by \"process.title\"", + "function": "rare", + "by_field_name": "process.title" + } ], - "description": "SIEM Auditbeat: Looks for an unusual web URL request from a Linux instance. Curl and wget web request activity is very common but unusual web requests from a Linux server can sometimes be malware delivery or execution (beta)", - "analysis_config": { - "bucket_span": "15m", - "detectors": [ - { - "detector_description": "rare by \"process.title\"", - "function": "rare", - "by_field_name": "process.title" - } - ], - "influencers": [ - "host.name", - "destination.ip", - "destination.port" - ] - }, - "allow_lazy_open": true, - "analysis_limits": { - "model_memory_limit": "32mb" - }, - "data_description": { - "time_field": "@timestamp" - }, - "custom_settings": { - "created_by": "ml-module-siem-auditbeat", - "custom_urls": [ - { - "url_name": "Host Details", - "url_value": "siem#/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$')))" - } - ] - } + "influencers": [ + "host.name", + "destination.ip", + "destination.port" + ] + }, + "allow_lazy_open": true, + "analysis_limits": { + "model_memory_limit": "32mb" + }, + "data_description": { + "time_field": "@timestamp" + }, + "custom_settings": { + "created_by": "ml-module-siem-auditbeat", + "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$')))" + } + ] } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_process_all_hosts_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_process_all_hosts_ecs.json index a204828d2669c..17f38b65de4c6 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_process_all_hosts_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_process_all_hosts_ecs.json @@ -1,8 +1,8 @@ { "job_type": "anomaly_detector", - "description": "SIEM Auditbeat: Looks for processes that are unusual to all Linux hosts. Such unusual processes may indicate unauthorized services, malware, or persistence mechanisms (beta)", + "description": "Security: Auditbeat - Looks for processes that are unusual to all Linux hosts. Such unusual processes may indicate unauthorized services, malware, or persistence mechanisms.", "groups": [ - "siem", + "security", "auditbeat", "process" ], @@ -33,20 +33,20 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/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_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": "siem#/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_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": "siem#/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_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": "siem#/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$')))" + "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$')))" } ] } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_user_name_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_user_name_ecs.json index c7c14a35054b2..8f0eda20a55fc 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_user_name_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_user_name_ecs.json @@ -1,11 +1,11 @@ { "job_type": "anomaly_detector", "groups": [ - "siem", + "security", "auditbeat", "process" ], - "description": "SIEM Auditbeat: 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 (beta)", + "description": "Security: Auditbeat - 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.", "analysis_config": { "bucket_span": "15m", "detectors": [ @@ -33,19 +33,19 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/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_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": "siem#/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_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": "siem#/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_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": "siem#/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$')))" + "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$')))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/rare_process_by_host_linux_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/rare_process_by_host_linux_ecs.json index aa9d49137c595..75ac0224dbd5b 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/rare_process_by_host_linux_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/rare_process_by_host_linux_ecs.json @@ -1,8 +1,8 @@ { "job_type": "anomaly_detector", - "description": "SIEM Auditbeat: Detect unusually rare processes on Linux (beta)", + "description": "Security: Auditbeat - Detect unusually rare processes on Linux", "groups": [ - "siem", + "security", "auditbeat", "process" ], @@ -34,20 +34,20 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/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_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": "siem#/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_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": "siem#/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_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": "siem#/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$')))" + "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$')))" } ] } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/manifest.json index 4b86752e45a92..f6e878de8169b 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/manifest.json @@ -1,7 +1,7 @@ { "id": "siem_auditbeat_auth", - "title": "SIEM Auditbeat Authentication", - "description": "Detect suspicious authentication events in Auditbeat data (beta).", + "title": "Security: Auditbeat Authentication", + "description": "Detect suspicious authentication events in Auditbeat data.", "type": "Auditbeat data", "logoFile": "logo.json", "defaultIndexPattern": "auditbeat-*", diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json index 4f48cd0ffc114..9ee26b314c640 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json @@ -1,8 +1,8 @@ { "job_type": "anomaly_detector", - "description": "SIEM Auditbeat: Detect unusually high number of authentication attempts (beta)", + "description": "Security: Auditbeat - Detect unusually high number of authentication attempts.", "groups": [ - "siem", + "security", "auditbeat", "authentication" ], @@ -33,8 +33,8 @@ "custom_urls": [ { "url_name": "IP Address Details", - "url_value": "siem#/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$')))" + "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$')))" } ] } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/manifest.json index b7afe8d2b158a..33940f20db903 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/manifest.json @@ -1,64 +1,64 @@ { - "id": "siem_cloudtrail", - "title": "SIEM Cloudtrail", - "description": "Detect suspicious activity recorded in your cloudtrail logs.", - "type": "Filebeat data", - "logoFile": "logo.json", - "defaultIndexPattern": "filebeat-*", - "query": { - "bool": { - "filter": [ - {"term": {"event.dataset": "aws.cloudtrail"}} - ] - } + "id": "siem_cloudtrail", + "title": "Security: Cloudtrail", + "description": "Detect suspicious activity recorded in your cloudtrail logs.", + "type": "Filebeat data", + "logoFile": "logo.json", + "defaultIndexPattern": "filebeat-*", + "query": { + "bool": { + "filter": [ + {"term": {"event.dataset": "aws.cloudtrail"}} + ] + } + }, + "jobs": [ + { + "id": "rare_method_for_a_city", + "file": "rare_method_for_a_city.json" }, - "jobs": [ - { - "id": "rare_method_for_a_city", - "file": "rare_method_for_a_city.json" - }, - { - "id": "rare_method_for_a_country", - "file": "rare_method_for_a_country.json" - }, - { - "id": "rare_method_for_a_username", - "file": "rare_method_for_a_username.json" - }, - { - "id": "high_distinct_count_error_message", - "file": "high_distinct_count_error_message.json" - }, - { - "id": "rare_error_code", - "file": "rare_error_code.json" - } - ], - "datafeeds": [ - { - "id": "datafeed-rare_method_for_a_city", - "file": "datafeed_rare_method_for_a_city.json", - "job_id": "rare_method_for_a_city" - }, - { - "id": "datafeed-rare_method_for_a_country", - "file": "datafeed_rare_method_for_a_country.json", - "job_id": "rare_method_for_a_country" - }, - { - "id": "datafeed-rare_method_for_a_username", - "file": "datafeed_rare_method_for_a_username.json", - "job_id": "rare_method_for_a_username" - }, - { - "id": "datafeed-high_distinct_count_error_message", - "file": "datafeed_high_distinct_count_error_message.json", - "job_id": "high_distinct_count_error_message" - }, - { - "id": "datafeed-rare_error_code", - "file": "datafeed_rare_error_code.json", - "job_id": "rare_error_code" - } - ] - } \ No newline at end of file + { + "id": "rare_method_for_a_country", + "file": "rare_method_for_a_country.json" + }, + { + "id": "rare_method_for_a_username", + "file": "rare_method_for_a_username.json" + }, + { + "id": "high_distinct_count_error_message", + "file": "high_distinct_count_error_message.json" + }, + { + "id": "rare_error_code", + "file": "rare_error_code.json" + } + ], + "datafeeds": [ + { + "id": "datafeed-rare_method_for_a_city", + "file": "datafeed_rare_method_for_a_city.json", + "job_id": "rare_method_for_a_city" + }, + { + "id": "datafeed-rare_method_for_a_country", + "file": "datafeed_rare_method_for_a_country.json", + "job_id": "rare_method_for_a_country" + }, + { + "id": "datafeed-rare_method_for_a_username", + "file": "datafeed_rare_method_for_a_username.json", + "job_id": "rare_method_for_a_username" + }, + { + "id": "datafeed-high_distinct_count_error_message", + "file": "datafeed_high_distinct_count_error_message.json", + "job_id": "high_distinct_count_error_message" + }, + { + "id": "datafeed-rare_error_code", + "file": "datafeed_rare_error_code.json", + "job_id": "rare_error_code" + } + ] +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/high_distinct_count_error_message.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/high_distinct_count_error_message.json index fdabf66ac91b3..98d145a91d9a7 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/high_distinct_count_error_message.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/high_distinct_count_error_message.json @@ -1,33 +1,33 @@ { - "job_type": "anomaly_detector", - "description": "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": [ - "siem", - "cloudtrail" + "job_type": "anomaly_detector", + "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" + ], + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "high_distinct_count(\"aws.cloudtrail.error_message\")", + "function": "high_distinct_count", + "field_name": "aws.cloudtrail.error_message" + } ], - "analysis_config": { - "bucket_span": "15m", - "detectors": [ - { - "detector_description": "high_distinct_count(\"aws.cloudtrail.error_message\")", - "function": "high_distinct_count", - "field_name": "aws.cloudtrail.error_message" - } - ], - "influencers": [ - "aws.cloudtrail.user_identity.arn", - "source.ip", - "source.geo.city_name" - ] - }, - "allow_lazy_open": true, - "analysis_limits": { - "model_memory_limit": "16mb" - }, - "data_description": { - "time_field": "@timestamp" - }, - "custom_settings": { - "created_by": "ml-module-siem-cloudtrail" - } - } \ No newline at end of file + "influencers": [ + "aws.cloudtrail.user_identity.arn", + "source.ip", + "source.geo.city_name" + ] + }, + "allow_lazy_open": true, + "analysis_limits": { + "model_memory_limit": "16mb" + }, + "data_description": { + "time_field": "@timestamp" + }, + "custom_settings": { + "created_by": "ml-module-siem-cloudtrail" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_error_code.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_error_code.json index a4ec84f1fb3f3..0227483f262a4 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_error_code.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_error_code.json @@ -1,33 +1,33 @@ { - "job_type": "anomaly_detector", - "description": "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": [ - "siem", - "cloudtrail" + "job_type": "anomaly_detector", + "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" + ], + "analysis_config": { + "bucket_span": "60m", + "detectors": [ + { + "detector_description": "rare by \"aws.cloudtrail.error_code\"", + "function": "rare", + "by_field_name": "aws.cloudtrail.error_code" + } ], - "analysis_config": { - "bucket_span": "60m", - "detectors": [ - { - "detector_description": "rare by \"aws.cloudtrail.error_code\"", - "function": "rare", - "by_field_name": "aws.cloudtrail.error_code" - } - ], - "influencers": [ - "aws.cloudtrail.user_identity.arn", - "source.ip", - "source.geo.city_name" - ] - }, - "allow_lazy_open": true, - "analysis_limits": { - "model_memory_limit": "16mb" - }, - "data_description": { - "time_field": "@timestamp" - }, - "custom_settings": { - "created_by": "ml-module-siem-cloudtrail" - } - } \ No newline at end of file + "influencers": [ + "aws.cloudtrail.user_identity.arn", + "source.ip", + "source.geo.city_name" + ] + }, + "allow_lazy_open": true, + "analysis_limits": { + "model_memory_limit": "16mb" + }, + "data_description": { + "time_field": "@timestamp" + }, + "custom_settings": { + "created_by": "ml-module-siem-cloudtrail" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_method_for_a_city.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_method_for_a_city.json index eff4d4cdbb889..228ad07d43532 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_method_for_a_city.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_method_for_a_city.json @@ -1,34 +1,34 @@ { - "job_type": "anomaly_detector", - "description": "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": [ - "siem", - "cloudtrail" + "job_type": "anomaly_detector", + "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" + ], + "analysis_config": { + "bucket_span": "60m", + "detectors": [ + { + "detector_description": "rare by \"event.action\" partition by \"source.geo.city_name\"", + "function": "rare", + "by_field_name": "event.action", + "partition_field_name": "source.geo.city_name" + } ], - "analysis_config": { - "bucket_span": "60m", - "detectors": [ - { - "detector_description": "rare by \"event.action\" partition by \"source.geo.city_name\"", - "function": "rare", - "by_field_name": "event.action", - "partition_field_name": "source.geo.city_name" - } - ], - "influencers": [ - "aws.cloudtrail.user_identity.arn", - "source.ip", - "source.geo.city_name" - ] - }, - "allow_lazy_open": true, - "analysis_limits": { - "model_memory_limit": "64mb" - }, - "data_description": { - "time_field": "@timestamp" - }, - "custom_settings": { - "created_by": "ml-module-siem-cloudtrail" - } - } \ No newline at end of file + "influencers": [ + "aws.cloudtrail.user_identity.arn", + "source.ip", + "source.geo.city_name" + ] + }, + "allow_lazy_open": true, + "analysis_limits": { + "model_memory_limit": "64mb" + }, + "data_description": { + "time_field": "@timestamp" + }, + "custom_settings": { + "created_by": "ml-module-siem-cloudtrail" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_method_for_a_country.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_method_for_a_country.json index 810822c30a5dd..fdba3ff12945c 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_method_for_a_country.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_method_for_a_country.json @@ -1,34 +1,34 @@ { - "job_type": "anomaly_detector", - "description": "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": [ - "siem", - "cloudtrail" + "job_type": "anomaly_detector", + "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" + ], + "analysis_config": { + "bucket_span": "60m", + "detectors": [ + { + "detector_description": "rare by \"event.action\" partition by \"source.geo.country_iso_code\"", + "function": "rare", + "by_field_name": "event.action", + "partition_field_name": "source.geo.country_iso_code" + } ], - "analysis_config": { - "bucket_span": "60m", - "detectors": [ - { - "detector_description": "rare by \"event.action\" partition by \"source.geo.country_iso_code\"", - "function": "rare", - "by_field_name": "event.action", - "partition_field_name": "source.geo.country_iso_code" - } - ], - "influencers": [ - "aws.cloudtrail.user_identity.arn", - "source.ip", - "source.geo.country_iso_code" - ] - }, - "allow_lazy_open": true, - "analysis_limits": { - "model_memory_limit": "64mb" - }, - "data_description": { - "time_field": "@timestamp" - }, - "custom_settings": { - "created_by": "ml-module-siem-cloudtrail" - } - } \ No newline at end of file + "influencers": [ + "aws.cloudtrail.user_identity.arn", + "source.ip", + "source.geo.country_iso_code" + ] + }, + "allow_lazy_open": true, + "analysis_limits": { + "model_memory_limit": "64mb" + }, + "data_description": { + "time_field": "@timestamp" + }, + "custom_settings": { + "created_by": "ml-module-siem-cloudtrail" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_method_for_a_username.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_method_for_a_username.json index 2edf52e8351ed..ea39a889a783e 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_method_for_a_username.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_method_for_a_username.json @@ -1,34 +1,34 @@ { - "job_type": "anomaly_detector", - "description": "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": [ - "siem", - "cloudtrail" + "job_type": "anomaly_detector", + "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" + ], + "analysis_config": { + "bucket_span": "60m", + "detectors": [ + { + "detector_description": "rare by \"event.action\" partition by \"user.name\"", + "function": "rare", + "by_field_name": "event.action", + "partition_field_name": "user.name" + } ], - "analysis_config": { - "bucket_span": "60m", - "detectors": [ - { - "detector_description": "rare by \"event.action\" partition by \"user.name\"", - "function": "rare", - "by_field_name": "event.action", - "partition_field_name": "user.name" - } - ], - "influencers": [ - "user.name", - "source.ip", - "source.geo.city_name" - ] - }, - "allow_lazy_open": true, - "analysis_limits": { - "model_memory_limit": "128mb" - }, - "data_description": { - "time_field": "@timestamp" - }, - "custom_settings": { - "created_by": "ml-module-siem-cloudtrail" - } - } \ No newline at end of file + "influencers": [ + "user.name", + "source.ip", + "source.geo.city_name" + ] + }, + "allow_lazy_open": true, + "analysis_limits": { + "model_memory_limit": "128mb" + }, + "data_description": { + "time_field": "@timestamp" + }, + "custom_settings": { + "created_by": "ml-module-siem-cloudtrail" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/manifest.json index 9109cbc15ca6f..e11e1726076d9 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/manifest.json @@ -1,7 +1,7 @@ { "id": "siem_packetbeat", - "title": "SIEM Packetbeat", - "description": "Detect suspicious network activity in Packetbeat data (beta).", + "title": "Security: Packetbeat", + "description": "Detect suspicious network activity in Packetbeat data.", "type": "Packetbeat data", "logoFile": "logo.json", "defaultIndexPattern": "packetbeat-*", diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json index 0f0fca1bf560a..0332fd53814a6 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json @@ -1,8 +1,8 @@ { "job_type": "anomaly_detector", - "description": "SIEM Packetbeat: Looks for unusual DNS activity that could indicate command-and-control or data exfiltration activity (beta)", + "description": "Security: Packetbeat - Looks for unusual DNS activity that could indicate command-and-control or data exfiltration activity.", "groups": [ - "siem", + "security", "packetbeat", "dns" ], @@ -48,7 +48,7 @@ "custom_urls": [ { "url_name": "Host Details", - "url_value": "siem#/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$')))" + "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$')))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_dns_question.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_dns_question.json index d2c4a0ca50dc4..c3c2402e13f72 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_dns_question.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_dns_question.json @@ -1,8 +1,8 @@ { "job_type": "anomaly_detector", - "description": "SIEM Packetbeat: Looks for unusual DNS activity that could indicate command-and-control activity (beta)", + "description": "Security: Packetbeat - Looks for unusual DNS activity that could indicate command-and-control activity.", "groups": [ - "siem", + "security", "packetbeat", "dns" ], @@ -31,7 +31,7 @@ "custom_urls": [ { "url_name": "Host Details", - "url_value": "siem#/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$')))" + "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$')))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_server_domain.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_server_domain.json index 132cf9fff04cc..14e01df1285d8 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_server_domain.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_server_domain.json @@ -1,8 +1,8 @@ { "job_type": "anomaly_detector", - "description": "SIEM Packetbeat: Looks for unusual HTTP or TLS destination domain activity that could indicate execution, persistence, command-and-control or data exfiltration activity (beta)", + "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": [ - "siem", + "security", "packetbeat", "web" ], @@ -33,7 +33,7 @@ "custom_urls": [ { "url_name": "Host Details", - "url_value": "siem#/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$')))" + "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$')))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_urls.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_urls.json index e0791ad4eaea9..ad664bed49c55 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_urls.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_urls.json @@ -1,8 +1,8 @@ { "job_type": "anomaly_detector", - "description": "SIEM Packetbeat: Looks for unusual web browsing URL activity that could indicate execution, persistence, command-and-control or data exfiltration activity (beta)", + "description": "Security: Packetbeat - Looks for unusual web browsing URL activity that could indicate execution, persistence, command-and-control or data exfiltration activity.", "groups": [ - "siem", + "security", "packetbeat", "web" ], @@ -32,7 +32,7 @@ "custom_urls": [ { "url_name": "Host Details", - "url_value": "siem#/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$')))" + "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$')))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_user_agent.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_user_agent.json index eae29466a6417..0dddf3e5d632e 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_user_agent.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_user_agent.json @@ -1,8 +1,8 @@ { "job_type": "anomaly_detector", - "description": "SIEM Packetbeat: Looks for unusual HTTP user agent activity that could indicate execution, persistence, command-and-control or data exfiltration activity (beta)", + "description": "Security: Packetbeat - Looks for unusual HTTP user agent activity that could indicate execution, persistence, command-and-control or data exfiltration activity.", "groups": [ - "siem", + "security", "packetbeat", "web" ], @@ -14,7 +14,7 @@ "function": "rare", "by_field_name": "user_agent.original" } - ], + ], "influencers": [ "host.name", "destination.ip" @@ -32,7 +32,7 @@ "custom_urls": [ { "url_name": "Host Details", - "url_value": "siem#/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$')))" + "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$')))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/manifest.json index 682b9a833f23f..ffbf5aa7d8bb0 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/manifest.json @@ -1,7 +1,7 @@ { "id": "siem_winlogbeat", - "title": "SIEM Winlogbeat", - "description": "Detect unusual processes and network activity in Winlogbeat data (beta).", + "title": "Security: Winlogbeat", + "description": "Detect unusual processes and network activity in Winlogbeat data.", "type": "Winlogbeat data", "logoFile": "logo.json", "defaultIndexPattern": "winlogbeat-*", diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/rare_process_by_host_windows_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/rare_process_by_host_windows_ecs.json index a0480a94e5356..49c936e33f70f 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/rare_process_by_host_windows_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/rare_process_by_host_windows_ecs.json @@ -1,8 +1,8 @@ { "job_type": "anomaly_detector", - "description": "SIEM Winlogbeat: Detect unusually rare processes on Windows (beta)", + "description": "Security: Winlogbeat - Detect unusually rare processes on Windows.", "groups": [ - "siem", + "security", "winlogbeat", "process" ], @@ -34,20 +34,20 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/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_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": "siem#/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_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": "siem#/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_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": "siem#/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$')))" + "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$')))" } ] } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_network_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_network_activity_ecs.json index c05b1a61e169a..d3fb038f85584 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_network_activity_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_network_activity_ecs.json @@ -1,8 +1,8 @@ { "job_type": "anomaly_detector", - "description": "SIEM Winlogbeat: Looks for unusual processes using the network which could indicate command-and-control, lateral movement, persistence, or data exfiltration activity (beta)", + "description": "Security: Winlogbeat - Looks for unusual processes using the network which could indicate command-and-control, lateral movement, persistence, or data exfiltration activity.", "groups": [ - "siem", + "security", "winlogbeat", "network" ], @@ -34,19 +34,19 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/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_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": "siem#/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_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": "siem#/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_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": "siem#/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$')))" + "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$')))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_path_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_path_activity_ecs.json index 7133335c44765..6a667527225a9 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_path_activity_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_path_activity_ecs.json @@ -1,11 +1,11 @@ { "job_type": "anomaly_detector", "groups": [ - "siem", + "security", "winlogbeat", "process" ], - "description": "SIEM Winlogbeat: Looks for activity in unusual paths that may indicate execution of malware or persistence mechanisms. Windows payloads often execute from user profile paths (beta)", + "description": "Security: Winlogbeat - Looks for activity in unusual paths that may indicate execution of malware or persistence mechanisms. Windows payloads often execute from user profile paths.", "analysis_config": { "bucket_span": "15m", "detectors": [ @@ -33,20 +33,20 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/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_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": "siem#/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_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": "siem#/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_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": "siem#/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$')))" + "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$')))" } ] } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_all_hosts_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_all_hosts_ecs.json index c99cb802ca249..9b23aa5a95e6c 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_all_hosts_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_all_hosts_ecs.json @@ -1,8 +1,8 @@ { "job_type": "anomaly_detector", - "description": "SIEM Winlogbeat: Looks for processes that are unusual to all Windows hosts. Such unusual processes may indicate execution of unauthorized services, malware, or persistence mechanisms (beta)", + "description": "Security: Winlogbeat - Looks for processes that are unusual to all Windows hosts. Such unusual processes may indicate execution of unauthorized services, malware, or persistence mechanisms.", "groups": [ - "siem", + "security", "winlogbeat", "process" ], @@ -33,19 +33,19 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/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_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": "siem#/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_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": "siem#/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_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": "siem#/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$')))" + "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$')))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_creation.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_creation.json index 98b17c2adb42e..9d90bba824418 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_creation.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_creation.json @@ -1,11 +1,11 @@ { "job_type": "anomaly_detector", "groups": [ - "siem", + "security", "winlogbeat", "process" ], - "description": "SIEM Winlogbeat: Looks for unusual process relationships which may indicate execution of malware or persistence mechanisms (beta)", + "description": "Security: Winlogbeat - Looks for unusual process relationships which may indicate execution of malware or persistence mechanisms.", "analysis_config": { "bucket_span": "15m", "detectors": [ @@ -33,20 +33,20 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/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_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": "siem#/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_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": "siem#/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_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": "siem#/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$')))" + "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$')))" } ] } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json index 9d98855c8e2c5..613a446750e5f 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json @@ -1,8 +1,8 @@ { "job_type": "anomaly_detector", - "description": "SIEM Winlogbeat: Looks for unusual powershell scripts that may indicate execution of malware, or persistence mechanisms (beta)", + "description": "Security: Winlogbeat - Looks for unusual powershell scripts that may indicate execution of malware, or persistence mechanisms.", "groups": [ - "siem", + "security", "winlogbeat", "powershell" ], @@ -33,12 +33,12 @@ "custom_urls": [ { "url_name": "Host Details by user name", - "url_value": "siem#/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_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": "siem#/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$')))" + "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$')))" } ] } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_service.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_service.json index 45b66aa7650cb..6debad30c308a 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_service.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_service.json @@ -1,11 +1,11 @@ { "job_type": "anomaly_detector", "groups": [ - "siem", - "winlogbeat", - "system" + "security", + "winlogbeat", + "system" ], - "description": "SIEM Winlogbeat: Looks for rare and unusual Windows services which may indicate execution of unauthorized services, malware, or persistence mechanisms (beta)", + "description": "Security: Winlogbeat - Looks for rare and unusual Windows services which may indicate execution of unauthorized services, malware, or persistence mechanisms.", "analysis_config": { "bucket_span": "15m", "detectors": [ @@ -32,7 +32,7 @@ "custom_urls": [ { "url_name": "Host Details", - "url_value": "siem#/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$')))" + "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$')))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_user_name_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_user_name_ecs.json index 10f60ca1aa4d8..7d9244a230ac3 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_user_name_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_user_name_ecs.json @@ -1,8 +1,8 @@ { "job_type": "anomaly_detector", - "description": "SIEM Winlogbeat: 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 (beta)", + "description": "Security: Winlogbeat - 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": [ - "siem", + "security", "winlogbeat", "process" ], @@ -33,19 +33,19 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/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_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": "siem#/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_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": "siem#/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_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": "siem#/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$')))" + "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$')))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_rare_user_runas_event.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_rare_user_runas_event.json index 20797827eee03..880be0045f84a 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_rare_user_runas_event.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_rare_user_runas_event.json @@ -1,8 +1,8 @@ { "job_type": "anomaly_detector", - "description": "SIEM Winlogbeat: Unusual user context switches can be due to privilege escalation (beta)", + "description": "Security: Winlogbeat - Unusual user context switches can be due to privilege escalation.", "groups": [ - "siem", + "security", "winlogbeat", "authentication" ], @@ -33,19 +33,19 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/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_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": "siem#/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_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": "siem#/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_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": "siem#/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$')))" + "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$')))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/manifest.json index b5e65e9638eb2..f08f4da880118 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/manifest.json @@ -1,7 +1,7 @@ { "id": "siem_winlogbeat_auth", - "title": "SIEM Winlogbeat Authentication", - "description": "Detect suspicious authentication events in Winlogbeat data (beta).", + "title": "Security: Winlogbeat Authentication", + "description": "Detect suspicious authentication events in Winlogbeat data.", "type": "Winlogbeat data", "logoFile": "logo.json", "defaultIndexPattern": "winlogbeat-*", diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/windows_rare_user_type10_remote_login.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/windows_rare_user_type10_remote_login.json index ee009e465ec23..c18bb7a151f53 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/windows_rare_user_type10_remote_login.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/windows_rare_user_type10_remote_login.json @@ -1,8 +1,8 @@ { "job_type": "anomaly_detector", - "description": "SIEM Winlogbeat Auth: Unusual RDP (remote desktop protocol) user logins can indicate account takeover or credentialed access (beta)", + "description": "Security: Winlogbeat Auth - Unusual RDP (remote desktop protocol) user logins can indicate account takeover or credentialed access.", "groups": [ - "siem", + "security", "winlogbeat", "authentication" ], @@ -33,19 +33,19 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/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_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": "siem#/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_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": "siem#/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_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": "siem#/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$')))" + "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$')))" } ] } From 18dcd24fe98b907a75e62b0d0a7c05136347bf3e Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 14 Jul 2020 17:59:00 -0700 Subject: [PATCH 63/82] [tests] Temporarily skipped to promote snapshot Will be re-enabled in #71727 Signed-off-by: Tyler Smalley --- x-pack/test/api_integration/apis/fleet/agents/enroll.ts | 4 +++- .../test/ingest_manager_api_integration/apis/epm/install.ts | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/fleet/agents/enroll.ts b/x-pack/test/api_integration/apis/fleet/agents/enroll.ts index e9f7471f6437e..d83b648fce0a9 100644 --- a/x-pack/test/api_integration/apis/fleet/agents/enroll.ts +++ b/x-pack/test/api_integration/apis/fleet/agents/enroll.ts @@ -21,7 +21,9 @@ export default function (providerContext: FtrProviderContext) { let apiKey: { id: string; api_key: string }; let kibanaVersion: string; - describe('fleet_agents_enroll', () => { + // Temporarily skipped to promote snapshot + // Re-enabled in https://github.com/elastic/kibana/pull/71727 + describe.skip('fleet_agents_enroll', () => { before(async () => { await esArchiver.loadIfNeeded('fleet/agents'); diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install.ts index f73ba56c172c4..54a7e0dcb9242 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/install.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install.ts @@ -21,6 +21,8 @@ export default function ({ getService }: FtrProviderContext) { const mappingsPackage = 'overrides-0.1.0'; const server = dockerServers.get('registry'); + // Temporarily skipped to promote snapshot + // Re-enabled in https://github.com/elastic/kibana/pull/71727 describe('installs packages that include settings and mappings overrides', async () => { after(async () => { if (server.enabled) { From a885f8ac1e5f80f784d3bd102ed66778d9e0b2d4 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 14 Jul 2020 21:09:05 -0400 Subject: [PATCH 64/82] [Ingest Manager] Better display of Fleet requirements (#71686) --- .../sections/fleet/setup_page/index.tsx | 306 +++++++++++++----- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 3 files changed, 234 insertions(+), 76 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/setup_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/setup_page/index.tsx index e9c9ce0c513d2..ffd8591a642c1 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/setup_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/setup_page/index.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPageBody, @@ -14,11 +15,39 @@ import { EuiTitle, EuiSpacer, EuiIcon, + EuiCallOut, + EuiFlexItem, + EuiFlexGroup, + EuiCode, + EuiCodeBlock, + EuiLink, } from '@elastic/eui'; import { useCore, sendPostFleetSetup } from '../../../hooks'; import { WithoutHeaderLayout } from '../../../layouts'; import { GetFleetStatusResponse } from '../../../types'; +export const RequirementItem: React.FunctionComponent<{ isMissing: boolean }> = ({ + isMissing, + children, +}) => { + return ( + + + + {isMissing ? ( + + ) : ( + + )} + + + + {children} + + + ); +}; + export const SetupPage: React.FunctionComponent<{ refresh: () => Promise; missingRequirements: GetFleetStatusResponse['missing_requirements']; @@ -26,8 +55,7 @@ export const SetupPage: React.FunctionComponent<{ const [isFormLoading, setIsFormLoading] = useState(false); const core = useCore(); - const onSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + const onSubmit = async () => { setIsFormLoading(true); try { await sendPostFleetSetup({ forceRecreate: true }); @@ -38,84 +66,218 @@ export const SetupPage: React.FunctionComponent<{ } }; - const content = - missingRequirements.includes('tls_required') || - missingRequirements.includes('api_keys') || - missingRequirements.includes('encrypted_saved_object_encryption_key_required') ? ( - <> - - - - -

+ if ( + !missingRequirements.includes('tls_required') && + !missingRequirements.includes('api_keys') && + !missingRequirements.includes('encrypted_saved_object_encryption_key_required') + ) { + return ( + + + + + + + +

+ +

+
+ + + + + + + + + + + +
+
+
+ ); + } + + return ( + + + + -

-
- - +
+ , - }} + id="xpack.ingestManager.setupPage.missingRequirementsElasticsearchTitle" + defaultMessage="In your Elasticsearch configuration, enable:" /> - - - - ) : ( - <> - - - - -

+ + + + + + ), + securityFlag: xpack.security.enabled, + true: true, + }} + /> + + + xpack.security.authc.api_key.enabled, + true: true, + apiKeyLink: ( + + + + ), + }} /> -

-
- - + + + + {`xpack.security.enabled: true +xpack.security.authc.api_key.enabled: true`} + + + + + + + + ), + securityFlag: xpack.security.enabled, + tlsLink: ( + + + + ), + tlsFlag: xpack.ingestManager.fleet.tlsCheckDisabled, + true: true, + }} + /> + + + + + + + ), + keyFlag: xpack.encryptedSavedObjects.encryptionKey, + }} + /> + + + + {`xpack.security.enabled: true +xpack.encryptedSavedObjects.encryptionKey: "something_at_least_32_characters"`} + + + + + + ), + }} /> - - - - - - - - - - - - ); - - return ( - - - - {content} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6ef8a61f93295..11aa191dbc7b7 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8367,8 +8367,6 @@ "xpack.ingestManager.setupPage.enableFleet": "ユーザーを作成してフリートを有効にます", "xpack.ingestManager.setupPage.enableText": "フリートを使用するには、Elasticユーザーを作成する必要があります。このユーザーは、APIキーを作成して、logs-*およびmetrics-*に書き込むことができます。", "xpack.ingestManager.setupPage.enableTitle": "フリートを有効にする", - "xpack.ingestManager.setupPage.missingRequirementsDescription": "Fleetを使用するには、次の機能を有効にする必要があります。{space}- Elasticsearch APIキーを有効にします。{space}- TLSを有効にして、エージェントKibanaの間の通信を保護します。 ", - "xpack.ingestManager.setupPage.missingRequirementsTitle": "見つからない要件", "xpack.ingestManager.unenrollAgents.confirmModal.cancelButtonLabel": "キャンセル", "xpack.ingestManager.unenrollAgents.confirmModal.confirmButtonLabel": "登録解除", "xpack.ingestManager.unenrollAgents.confirmModal.deleteMultipleTitle": "{count, plural, one {# エージェント} other {# エージェント}}の登録を解除しますか?", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3c8016d64248b..c753c2586093e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8372,8 +8372,6 @@ "xpack.ingestManager.setupPage.enableFleet": "创建用户并启用 Fleet", "xpack.ingestManager.setupPage.enableText": "要使用 Fleet,必须创建 Elastic 用户。此用户可以创建 API 密钥并写入到 logs-* and metrics-*。", "xpack.ingestManager.setupPage.enableTitle": "启用 Fleet", - "xpack.ingestManager.setupPage.missingRequirementsDescription": "要使用 Fleet,必须启用以下功能:{space}- 启用 Elasticsearch API 密钥。{space}- 启用 TLS 以保护代理和 Kibana 之间的通信。 ", - "xpack.ingestManager.setupPage.missingRequirementsTitle": "缺失的要求", "xpack.ingestManager.unenrollAgents.confirmModal.cancelButtonLabel": "取消", "xpack.ingestManager.unenrollAgents.confirmModal.confirmButtonLabel": "取消注册", "xpack.ingestManager.unenrollAgents.confirmModal.deleteMultipleTitle": "取消注册 {count, plural, one {# 个代理} other {# 个代理}}?", From 56de45d156be23069815fec17440cf978710451f Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Tue, 14 Jul 2020 21:27:44 -0400 Subject: [PATCH 65/82] [Security Solution] [Detections] Fixes bug for determining when we hit max signals after filtering with lists (#71768) update signal counter with filtered results, not with direct search results. --- .../signals/filter_events_with_list.ts | 1 - .../signals/search_after_bulk_create.ts | 16 ++++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts index f16de8bf05ef4..8af08a02f4152 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts @@ -31,7 +31,6 @@ export const filterEventsAgainstList = async ({ buildRuleMessage, }: FilterEventsAgainstList): Promise => { try { - logger.debug(buildRuleMessage(`exceptionsList: ${JSON.stringify(exceptionsList, null, 2)}`)); if (exceptionsList == null || exceptionsList.length === 0) { logger.debug(buildRuleMessage('about to return original search result')); return eventSearchResult; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index 2a0e39cbbf237..cd6beb9c68ab2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -91,7 +91,7 @@ export const searchAfterAndBulkCreate = async ({ }; let sortId; // tells us where to start our next search_after query - let searchResultSize = 0; + let signalsCreatedCount = 0; /* The purpose of `maxResults` is to ensure we do not perform @@ -127,8 +127,8 @@ export const searchAfterAndBulkCreate = async ({ toReturn.success = false; return toReturn; } - searchResultSize = 0; - while (searchResultSize < tuple.maxSignals) { + signalsCreatedCount = 0; + while (signalsCreatedCount < tuple.maxSignals) { try { logger.debug(buildRuleMessage(`sortIds: ${sortId}`)); const { @@ -167,7 +167,6 @@ export const searchAfterAndBulkCreate = async ({ searchResult.hits.hits[searchResult.hits.hits.length - 1]?._source['@timestamp'] ) : null; - searchResultSize += searchResult.hits.hits.length; // filter out the search results that match with the values found in the list. // the resulting set are valid signals that are not on the allowlist. @@ -187,6 +186,14 @@ export const searchAfterAndBulkCreate = async ({ break; } + // make sure we are not going to create more signals than maxSignals allows + if (signalsCreatedCount + filteredEvents.hits.hits.length > tuple.maxSignals) { + filteredEvents.hits.hits = filteredEvents.hits.hits.slice( + 0, + tuple.maxSignals - signalsCreatedCount + ); + } + const { bulkCreateDuration: bulkDuration, createdItemsCount: createdCount, @@ -211,6 +218,7 @@ export const searchAfterAndBulkCreate = async ({ }); logger.debug(buildRuleMessage(`created ${createdCount} signals`)); toReturn.createdSignalsCount += createdCount; + signalsCreatedCount += createdCount; if (bulkDuration) { toReturn.bulkCreateTimes.push(bulkDuration); } From 0d1c166a4622c31de4824e25170125d8141355ad Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 14 Jul 2020 19:01:31 -0700 Subject: [PATCH 66/82] [Reporting] Re-delete a file (#71730) ...that was accidentally recovered due to incorrect manual merge --- .../csv_from_savedobject/execute_job.ts | 12 +---- .../lib/get_fake_request.ts | 51 ------------------- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 4 files changed, 1 insertion(+), 66 deletions(-) delete mode 100644 x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_fake_request.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts index ffe453f996698..0cc9ec16ed71b 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts @@ -10,7 +10,6 @@ import { CONTENT_TYPE_CSV, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common import { RunTaskFnFactory, ScheduledTaskParams, TaskRunResult } from '../../types'; import { createGenerateCsv } from '../csv/generate_csv'; import { JobParamsPanelCsv, SearchPanel } from './types'; -import { getFakeRequest } from './lib/get_fake_request'; import { getGenerateCsvParams } from './lib/get_csv_job'; /* @@ -44,19 +43,10 @@ export const runTaskFnFactory: RunTaskFnFactory = function e const { jobParams } = jobPayload; const jobLogger = logger.clone([jobId === null ? 'immediate' : jobId]); const generateCsv = createGenerateCsv(jobLogger); - const { isImmediate, panel, visType } = jobParams as JobParamsPanelCsv & { - panel: SearchPanel; - }; + const { panel, visType } = jobParams as JobParamsPanelCsv & { panel: SearchPanel }; jobLogger.debug(`Execute job generating [${visType}] csv`); - if (isImmediate && req) { - jobLogger.info(`Executing job from Immediate API using request context`); - } else { - jobLogger.info(`Executing job async using encrypted headers`); - req = await getFakeRequest(jobPayload, config.get('encryptionKey')!, jobLogger); - } - const savedObjectsClient = context.core.savedObjects.client; const uiConfig = await reporting.getUiSettingsServiceFactory(savedObjectsClient); diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_fake_request.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_fake_request.ts deleted file mode 100644 index 3afbaa650e6c8..0000000000000 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_fake_request.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { KibanaRequest } from 'kibana/server'; -import { cryptoFactory, LevelLogger } from '../../../lib'; -import { ScheduledTaskParams } from '../../../types'; -import { JobParamsPanelCsv } from '../types'; - -export const getFakeRequest = async ( - job: ScheduledTaskParams, - encryptionKey: string, - jobLogger: LevelLogger -) => { - // TODO remove this block: csv from savedobject download is always "sync" - const crypto = cryptoFactory(encryptionKey); - let decryptedHeaders: KibanaRequest['headers']; - const serializedEncryptedHeaders = job.headers; - try { - if (typeof serializedEncryptedHeaders !== 'string') { - throw new Error( - i18n.translate( - 'xpack.reporting.exportTypes.csv_from_savedobject.executeJob.missingJobHeadersErrorMessage', - { - defaultMessage: 'Job headers are missing', - } - ) - ); - } - decryptedHeaders = (await crypto.decrypt( - serializedEncryptedHeaders - )) as KibanaRequest['headers']; - } catch (err) { - jobLogger.error(err); - throw new Error( - i18n.translate( - 'xpack.reporting.exportTypes.csv_from_savedobject.executeJob.failedToDecryptReportJobDataErrorMessage', - { - defaultMessage: - 'Failed to decrypt report job data. Please ensure that {encryptionKey} is set and re-generate this report. {err}', - values: { encryptionKey: 'xpack.reporting.encryptionKey', err }, - } - ) - ); - } - - return { headers: decryptedHeaders } as KibanaRequest; -}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 11aa191dbc7b7..b9d2fdcbbfca7 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -12288,8 +12288,6 @@ "xpack.reporting.errorButton.unableToGenerateReportTitle": "レポートを生成できません", "xpack.reporting.exportTypes.common.failedToDecryptReportJobDataErrorMessage": "レポートジョブデータの解読に失敗しました。{encryptionKey}が設定されていることを確認してこのレポートを再生成してください。{err}", "xpack.reporting.exportTypes.common.missingJobHeadersErrorMessage": "ジョブヘッダーがありません", - "xpack.reporting.exportTypes.csv_from_savedobject.executeJob.failedToDecryptReportJobDataErrorMessage": "レポートジョブデータの解読に失敗しました{encryptionKey} が設定されていることを確認してこのレポートを再生成してください。{err}", - "xpack.reporting.exportTypes.csv_from_savedobject.executeJob.missingJobHeadersErrorMessage": "ジョブヘッダーがありません", "xpack.reporting.exportTypes.csv.executeJob.dateFormateSetting": "Kibana の高度な設定「{dateFormatTimezone}」が「ブラウザー」に設定されていますあいまいさを避けるために日付は UTC 形式に変換されます。", "xpack.reporting.exportTypes.csv.executeJob.failedToDecryptReportJobDataErrorMessage": "レポートジョブデータの解読に失敗しました{encryptionKey} が設定されていることを確認してこのレポートを再生成してください。{err}", "xpack.reporting.exportTypes.csv.executeJob.missingJobHeadersErrorMessage": "ジョブヘッダーがありません", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c753c2586093e..b45f02f41d11f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12294,8 +12294,6 @@ "xpack.reporting.errorButton.unableToGenerateReportTitle": "无法生成报告", "xpack.reporting.exportTypes.common.failedToDecryptReportJobDataErrorMessage": "无法解密报告作业数据。请确保已设置 {encryptionKey},然后重新生成此报告。{err}", "xpack.reporting.exportTypes.common.missingJobHeadersErrorMessage": "作业标头缺失", - "xpack.reporting.exportTypes.csv_from_savedobject.executeJob.failedToDecryptReportJobDataErrorMessage": "无法解密报告作业数据。请确保已设置 {encryptionKey},然后重新生成此报告。{err}", - "xpack.reporting.exportTypes.csv_from_savedobject.executeJob.missingJobHeadersErrorMessage": "作业标头缺失", "xpack.reporting.exportTypes.csv.executeJob.dateFormateSetting": "Kibana 高级设置“{dateFormatTimezone}”已设置为“浏览器”。日期将格式化为 UTC 以避免混淆。", "xpack.reporting.exportTypes.csv.executeJob.failedToDecryptReportJobDataErrorMessage": "无法解密报告作业数据。请确保已设置 {encryptionKey},然后重新生成此报告。{err}", "xpack.reporting.exportTypes.csv.executeJob.missingJobHeadersErrorMessage": "作业标头缺失", From 8a9988093eb4a7486d09aac8c894c2ac9e672f76 Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Tue, 14 Jul 2020 22:04:59 -0400 Subject: [PATCH 67/82] [Security Solution][Exceptions] - Adds filtering to endpoint index patterns by exceptional fields (#71757) --- .../components/exceptions/builder/index.tsx | 15 ++- .../exceptions/exceptionable_fields.json | 127 ++++++++++++++++++ 2 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx index d3ed1dfc944fd..6bff33afaf70c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx @@ -23,6 +23,8 @@ import { BuilderButtonOptions } from './builder_button_options'; import { getNewExceptionItem, filterExceptionItems } from '../helpers'; import { ExceptionsBuilderExceptionItem, CreateExceptionListItemBuilderSchema } from '../types'; import { Loader } from '../../loader'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import exceptionableFields from '../exceptionable_fields.json'; const MyInvisibleAndBadge = styled(EuiFlexItem)` visibility: hidden; @@ -172,6 +174,17 @@ export const ExceptionBuilder = ({ ); }, [exceptions]); + // Filters index pattern fields by exceptionable fields if list type is endpoint + const filterIndexPatterns = useCallback(() => { + if (listType === 'endpoint') { + return { + ...indexPatterns, + fields: indexPatterns.fields.filter(({ name }) => exceptionableFields.includes(name)), + }; + } + return indexPatterns; + }, [indexPatterns, listType]); + // The builder can have existing exception items, or new exception items that have yet // to be created (and thus lack an id), this was creating some React bugs with relying // on the index, as a result, created a temporary id when new exception items are first @@ -216,7 +229,7 @@ export const ExceptionBuilder = ({ key={getExceptionListItemId(exceptionListItem, index)} exceptionItem={exceptionListItem} exceptionId={getExceptionListItemId(exceptionListItem, index)} - indexPattern={indexPatterns} + indexPattern={filterIndexPatterns()} isLoading={indexPatternLoading} exceptionItemIndex={index} andLogicIncluded={andLogicIncluded} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json new file mode 100644 index 0000000000000..18257b0de0a17 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json @@ -0,0 +1,127 @@ +[ + "Endpoint.policy.applied.id", + "Target.process.Ext.code_signature.status", + "Target.process.Ext.code_signature.subject_name", + "Target.process.Ext.code_signature.trusted", + "Target.process.Ext.code_signature.valid", + "Target.process.Ext.services", + "Target.process.Ext.user", + "Target.process.command_line", + "Target.process.executable", + "Target.process.hash.md5", + "Target.process.hash.sha1", + "Target.process.hash.sha256", + "Target.process.hash.sha512", + "Target.process.name", + "Target.process.parent.Ext.code_signature.status", + "Target.process.parent.Ext.code_signature.subject_name", + "Target.process.parent.Ext.code_signature.trusted", + "Target.process.parent.Ext.code_signature.valid", + "Target.process.parent.command_line", + "Target.process.parent.executable", + "Target.process.parent.hash.md5", + "Target.process.parent.hash.sha1", + "Target.process.parent.hash.sha256", + "Target.process.parent.hash.sha512", + "Target.process.parent.name", + "Target.process.parent.pgid", + "Target.process.parent.working_directory", + "Target.process.pe.company", + "Target.process.pe.description", + "Target.process.pe.file_version", + "Target.process.pe.original_file_name", + "Target.process.pe.product", + "Target.process.pgid", + "Target.process.working_directory", + "agent.id", + "agent.type", + "agent.version", + "elastic.agent.id", + "event.action", + "event.category", + "event.code", + "event.hash", + "event.kind", + "event.module", + "event.outcome", + "event.provider", + "event.type", + "file.Ext.code_signature.status", + "file.Ext.code_signature.subject_name", + "file.Ext.code_signature.trusted", + "file.Ext.code_signature.valid", + "file.attributes", + "file.device", + "file.directory", + "file.drive_letter", + "file.extension", + "file.gid", + "file.group", + "file.hash.md5", + "file.hash.sha1", + "file.hash.sha256", + "file.hash.sha512", + "file.inode", + "file.mime_type", + "file.mode", + "file.name", + "file.owner", + "file.path", + "file.pe.company", + "file.pe.description", + "file.pe.file_version", + "file.pe.original_file_name", + "file.pe.product", + "file.size", + "file.target_path", + "file.type", + "file.uid", + "group.Ext.real.id", + "group.domain", + "group.id", + "host.architecture", + "host.domain", + "host.id", + "host.os.Ext.variant", + "host.os.family", + "host.os.full", + "host.os.kernel", + "host.os.name", + "host.os.platform", + "host.os.version", + "host.type", + "process.Ext.code_signature.status", + "process.Ext.code_signature.subject_name", + "process.Ext.code_signature.trusted", + "process.Ext.code_signature.valid", + "process.Ext.services", + "process.Ext.user", + "process.command_line", + "process.executable", + "process.hash.md5", + "process.hash.sha1", + "process.hash.sha256", + "process.hash.sha512", + "process.name", + "process.parent.Ext.code_signature.status", + "process.parent.Ext.code_signature.subject_name", + "process.parent.Ext.code_signature.trusted", + "process.parent.Ext.code_signature.valid", + "process.parent.command_line", + "process.parent.executable", + "process.parent.hash.md5", + "process.parent.hash.sha1", + "process.parent.hash.sha256", + "process.parent.hash.sha512", + "process.parent.name", + "process.parent.pgid", + "process.parent.working_directory", + "process.pe.company", + "process.pe.description", + "process.pe.file_version", + "process.pe.original_file_name", + "process.pe.product", + "process.pgid", + "process.working_directory", + "rule.uuid" +] \ No newline at end of file From 73f5dec3db901dc31a096d3f0e6285adf2c01e2f Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Tue, 14 Jul 2020 21:20:19 -0500 Subject: [PATCH 68/82] Skip jest tests that timeout waiting for react (#71801) --- .../components/value_lists_management_modal/modal.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.test.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.test.tsx index daf1cbd68df91..ab2bc9b2e90e1 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.test.tsx @@ -11,7 +11,8 @@ import { TestProviders } from '../../../common/mock'; import { ValueListsModal } from './modal'; import { waitForUpdates } from '../../../common/utils/test_utils'; -describe('ValueListsModal', () => { +// TODO: These are occasionally timing out +describe.skip('ValueListsModal', () => { it('renders nothing if showModal is false', () => { const container = mount( From c5e39a24cda51f1062592cfc2d203b60e64832c4 Mon Sep 17 00:00:00 2001 From: Marshall Main <55718608+marshallmain@users.noreply.github.com> Date: Tue, 14 Jul 2020 22:25:10 -0400 Subject: [PATCH 69/82] Add endpoint exception creation API validation (#71791) --- .../create_exception_list_item_route.ts | 17 + .../routes/endpoint_disallowed_fields.ts | 13 + x-pack/test/api_integration/apis/index.js | 1 + .../apis/lists/create_exception_list_item.ts | 72 + .../test/api_integration/apis/lists/index.ts | 13 + .../functional/es_archives/lists/data.json | 85 + .../es_archives/lists/mappings.json | 2491 +++++++++++++++++ 7 files changed, 2692 insertions(+) create mode 100644 x-pack/plugins/lists/server/routes/endpoint_disallowed_fields.ts create mode 100644 x-pack/test/api_integration/apis/lists/create_exception_list_item.ts create mode 100644 x-pack/test/api_integration/apis/lists/index.ts create mode 100644 x-pack/test/functional/es_archives/lists/data.json create mode 100644 x-pack/test/functional/es_archives/lists/mappings.json diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts index 375d25c6fa5f8..c331eeb4bd2d0 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts @@ -16,6 +16,7 @@ import { } from '../../common/schemas'; import { getExceptionListClient } from './utils/get_exception_list_client'; +import { endpointDisallowedFields } from './endpoint_disallowed_fields'; export const createExceptionListItemRoute = (router: IRouter): void => { router.post( @@ -70,6 +71,22 @@ export const createExceptionListItemRoute = (router: IRouter): void => { statusCode: 409, }); } else { + if (exceptionList.type === 'endpoint') { + for (const entry of entries) { + if (entry.type === 'list') { + return siemResponse.error({ + body: `cannot add exception item with entry of type "list" to endpoint exception list`, + statusCode: 400, + }); + } + if (endpointDisallowedFields.includes(entry.field)) { + return siemResponse.error({ + body: `cannot add endpoint exception item on field ${entry.field}`, + statusCode: 400, + }); + } + } + } const createdList = await exceptionLists.createExceptionListItem({ _tags, comments, diff --git a/x-pack/plugins/lists/server/routes/endpoint_disallowed_fields.ts b/x-pack/plugins/lists/server/routes/endpoint_disallowed_fields.ts new file mode 100644 index 0000000000000..cf3389351f61d --- /dev/null +++ b/x-pack/plugins/lists/server/routes/endpoint_disallowed_fields.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +export const endpointDisallowedFields = [ + 'file.Ext.quarantine_path', + 'file.Ext.quarantine_result', + 'process.entity_id', + 'process.parent.entity_id', + 'process.ancestry', +]; diff --git a/x-pack/test/api_integration/apis/index.js b/x-pack/test/api_integration/apis/index.js index 3f3294c85d6df..aeea062bdb85d 100644 --- a/x-pack/test/api_integration/apis/index.js +++ b/x-pack/test/api_integration/apis/index.js @@ -31,5 +31,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./transform')); loadTestFile(require.resolve('./endpoint')); loadTestFile(require.resolve('./ingest_manager')); + loadTestFile(require.resolve('./lists')); }); } diff --git a/x-pack/test/api_integration/apis/lists/create_exception_list_item.ts b/x-pack/test/api_integration/apis/lists/create_exception_list_item.ts new file mode 100644 index 0000000000000..41f2a2dd2e3f5 --- /dev/null +++ b/x-pack/test/api_integration/apis/lists/create_exception_list_item.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect/expect.js'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + describe('Lists API', () => { + before(async () => await esArchiver.load('lists')); + + after(async () => await esArchiver.unload('lists')); + + it('should return a 400 if an endpoint exception item with a list-based entry is provided', async () => { + const badItem = { + namespace_type: 'agnostic', + description: 'bad endpoint item for testing', + name: 'bad endpoint item', + list_id: 'endpoint_list', + type: 'simple', + entries: [ + { + type: 'list', + field: 'some.field', + operator: 'included', + list: { + id: 'somelist', + type: 'keyword', + }, + }, + ], + }; + const { body } = await supertest + .post(`/api/exception_lists/items`) + .set('kbn-xsrf', 'xxx') + .send(badItem) + .expect(400); + expect(body.message).to.eql( + 'cannot add exception item with entry of type "list" to endpoint exception list' + ); + }); + + it('should return a 400 if endpoint exception entry has disallowed field', async () => { + const fieldName = 'file.Ext.quarantine_path'; + const badItem = { + namespace_type: 'agnostic', + description: 'bad endpoint item for testing', + name: 'bad endpoint item', + list_id: 'endpoint_list', + type: 'simple', + entries: [ + { + type: 'match', + field: fieldName, + operator: 'included', + value: 'doesnt matter', + }, + ], + }; + const { body } = await supertest + .post(`/api/exception_lists/items`) + .set('kbn-xsrf', 'xxx') + .send(badItem) + .expect(400); + expect(body.message).to.eql(`cannot add endpoint exception item on field ${fieldName}`); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/lists/index.ts b/x-pack/test/api_integration/apis/lists/index.ts new file mode 100644 index 0000000000000..73523c13bfc0a --- /dev/null +++ b/x-pack/test/api_integration/apis/lists/index.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function listsAPIIntegrationTests({ loadTestFile }: FtrProviderContext) { + describe('Lists plugin', function () { + this.tags(['lists']); + loadTestFile(require.resolve('./create_exception_list_item')); + }); +} diff --git a/x-pack/test/functional/es_archives/lists/data.json b/x-pack/test/functional/es_archives/lists/data.json new file mode 100644 index 0000000000000..eabc721f4887e --- /dev/null +++ b/x-pack/test/functional/es_archives/lists/data.json @@ -0,0 +1,85 @@ +{ + "type": "doc", + "value": { + "id": "exception-list-agnostic:1", + "index": ".kibana", + "source": { + "type": "exception-list-agnostic", + "exception-list-agnostic": { + "_tags": [ + "endpoint", + "process", + "malware", + "os:linux" + ], + "created_at": "2020-04-23T00:19:13.289Z", + "created_by": "user_name", + "description": "This is a sample endpoint type exception list", + "list_id": "endpoint_list", + "list_type": "list", + "name": "Sample Endpoint Exception List", + "tags": [ + "user added string for a tag", + "malware" + ], + "tie_breaker_id": "77fd1909-6786-428a-a671-30229a719c1f", + "type": "endpoint", + "updated_by": "user_name" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "exception-list-agnostic:2", + "index": ".kibana", + "source": { + "type": "exception-list-agnostic", + "exception-list-agnostic": { + "_tags": [ + "endpoint", + "process", + "malware", + "os:linux" + ], + "comments": [], + "created_at": "2020-04-23T00:19:13.289Z", + "created_by": "user_name", + "description": "This is a sample endpoint type exception", + "entries": [ + { + "entries": [ + { + "field": "nested.field", + "operator": "included", + "type": "match", + "value": "some value" + } + ], + "field": "some.parentField", + "type": "nested" + }, + { + "field": "some.not.nested.field", + "operator": "included", + "type": "match", + "value": "some value" + } + ], + "item_id": "endpoint_list_item", + "list_id": "endpoint_list", + "list_type": "item", + "name": "Sample Endpoint Exception List", + "tags": [ + "user added string for a tag", + "malware" + ], + "tie_breaker_id": "77fd1909-6786-428a-a671-30229a719c1f", + "type": "simple", + "updated_by": "user_name" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/lists/mappings.json b/x-pack/test/functional/es_archives/lists/mappings.json new file mode 100644 index 0000000000000..c1b277b8183a3 --- /dev/null +++ b/x-pack/test/functional/es_archives/lists/mappings.json @@ -0,0 +1,2491 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": {} + }, + "index": ".kibana_1", + "mappings": { + "dynamic": "strict", + "_meta": { + "migrationMappingPropertyHashes": { + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "visualization": "52d7a13ad68a150c4525b292d23e12cc", + "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "epm-packages": "04696e7dba1b9597f7d6ed78a4a76658", + "type": "2f4316de49999235636386fe51dc06c1", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "infrastructure-ui-source": "2b2809653635caf490c93f090502d04c", + "ingest_manager_settings": "012cf278ec84579495110bb827d1ed09", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "action": "6e96ac5e648f57523879661ea72525b7", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "metrics-explorer-view": "a8df1d270ee48c969d22d23812d08187", + "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "application_usage_transactional": "43b8830d5d0df85a6823d290885fc9fd", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "fleet-agent-events": "3231653fafe4ef3196fe3b32ab774bf2", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2", + "upgrade-assistant-reindex-operation": "296a89039fc4260292be36b1b005d8f2", + "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "fleet-enrollment-api-keys": "28b91e20b105b6f928e2012600085d8f", + "canvas-element": "7390014e1091044523666d97247392fc", + "ingest-outputs": "8aa988c376e65443fefc26f1075e93a3", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "lens": "d33c68a69ff1e78c9888dedd2164ac22", + "exception-list-agnostic": "4818e7dfc3e538562c80ec34eb6f841b", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "fleet-agent-actions": "e520c855577170c24481be05c3ae14ec", + "exception-list": "4818e7dfc3e538562c80ec34eb6f841b", + "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "search": "5c4b9a6effceb17ae8a0ab22d0c49767", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "alert": "7b44fba6773e37c806ce290ea9b7024e", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "map": "4a05b35c3a3a58fbc72dd0202dc3487f", + "uptime-dynamic-settings": "fcdb453a30092f022f2642db29523d80", + "cases": "32aa96a6d3855ddda53010ae2048ac22", + "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "siem-ui-timeline": "94bc38c7a421d15fbfe8ea565370a421", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "ingest-agent-configs": "9326f99c977fd2ef5ab24b6336a0675c", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "endpoint:user-artifact-manifest": "67c28185da541c1404e7852d30498cd6", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "fleet-agents": "034346488514b7058a79140b19ddf631", + "maps-telemetry": "5ef305b18111b77789afefbd36b66171", + "namespace": "2f4316de49999235636386fe51dc06c1", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "ingest-package-configs": "48e8bd97e488008e21c0b5a2367b83ad", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" + } + }, + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "type": "object", + "enabled": false + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "type": "object", + "enabled": false + } + } + }, + "alert": { + "properties": { + "actions": { + "type": "nested", + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "type": "object", + "enabled": false + } + } + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + }, + "params": { + "type": "object", + "enabled": false + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-telemetry": { + "type": "object", + "dynamic": "false" + }, + "app_search_telemetry": { + "type": "object", + "dynamic": "false" + }, + "application_usage_totals": { + "type": "object", + "dynamic": "false" + }, + "application_usage_transactional": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "canvas-workpad-template": { + "dynamic": "false", + "properties": { + "help": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + }, + "tags": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + }, + "template_key": { + "type": "keyword" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "connector_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "false", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "endpoint:user-artifact": { + "properties": { + "body": { + "type": "binary" + }, + "compressionAlgorithm": { + "type": "keyword", + "index": false + }, + "created": { + "type": "date", + "index": false + }, + "decodedSha256": { + "type": "keyword", + "index": false + }, + "decodedSize": { + "type": "long", + "index": false + }, + "encodedSha256": { + "type": "keyword" + }, + "encodedSize": { + "type": "long", + "index": false + }, + "encryptionAlgorithm": { + "type": "keyword", + "index": false + }, + "identifier": { + "type": "keyword" + } + } + }, + "endpoint:user-artifact-manifest": { + "properties": { + "created": { + "type": "date", + "index": false + }, + "ids": { + "type": "keyword", + "index": false + } + } + }, + "epm-packages": { + "properties": { + "es_index_patterns": { + "type": "object", + "enabled": false + }, + "installed": { + "type": "nested", + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "removable": { + "type": "boolean" + }, + "version": { + "type": "keyword" + } + } + }, + "exception-list": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "keyword", + "fields": { + "text": { + "type": "text" + } + } + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "keyword", + "fields": { + "text": { + "type": "text" + } + } + } + } + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "exception-list-agnostic": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "keyword", + "fields": { + "text": { + "type": "text" + } + } + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "keyword", + "fields": { + "text": { + "type": "text" + } + } + } + } + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "fleet-agent-actions": { + "properties": { + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "binary" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agent-events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "config_id": { + "type": "keyword" + }, + "config_revision": { + "type": "integer" + }, + "current_error_events": { + "type": "text", + "index": false + }, + "default_api_key": { + "type": "binary" + }, + "default_api_key_id": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_checkin_status": { + "type": "keyword" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "flattened" + }, + "packages": { + "type": "keyword" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "unenrolled_at": { + "type": "date" + }, + "unenrollment_started_at": { + "type": "date" + }, + "updated_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "flattened" + }, + "version": { + "type": "keyword" + } + } + }, + "fleet-enrollment-api-keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "infrastructure-ui-source": { + "properties": { + "description": { + "type": "text" + }, + "fields": { + "properties": { + "container": { + "type": "keyword" + }, + "host": { + "type": "keyword" + }, + "pod": { + "type": "keyword" + }, + "tiebreaker": { + "type": "keyword" + }, + "timestamp": { + "type": "keyword" + } + } + }, + "inventoryDefaultView": { + "type": "keyword" + }, + "logAlias": { + "type": "keyword" + }, + "logColumns": { + "type": "nested", + "properties": { + "fieldColumn": { + "properties": { + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + } + } + }, + "messageColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "timestampColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + } + } + }, + "metricAlias": { + "type": "keyword" + }, + "metricsExplorerDefaultView": { + "type": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "ingest-agent-configs": { + "properties": { + "description": { + "type": "text" + }, + "is_default": { + "type": "boolean" + }, + "monitoring_enabled": { + "type": "keyword", + "index": false + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "package_configs": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest-outputs": { + "properties": { + "ca_sha256": { + "type": "keyword", + "index": false + }, + "config": { + "type": "flattened" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "ingest-package-configs": { + "properties": { + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "type": "nested", + "enabled": false, + "properties": { + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "streams": { + "type": "nested", + "properties": { + "compiled_stream": { + "type": "flattened" + }, + "config": { + "type": "flattened" + }, + "dataset": { + "properties": { + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + } + }, + "type": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + } + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "revision": { + "type": "integer" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest_manager_settings": { + "properties": { + "agent_auto_upgrade": { + "type": "keyword" + }, + "has_seen_add_data_notice": { + "type": "boolean", + "index": false + }, + "kibana_ca_sha256": { + "type": "keyword" + }, + "kibana_url": { + "type": "keyword" + }, + "package_auto_upgrade": { + "type": "keyword" + } + } + }, + "inventory-view": { + "properties": { + "accountId": { + "type": "keyword" + }, + "autoBounds": { + "type": "boolean" + }, + "autoReload": { + "type": "boolean" + }, + "boundsOverride": { + "properties": { + "max": { + "type": "integer" + }, + "min": { + "type": "integer" + } + } + }, + "customMetrics": { + "type": "nested", + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "customOptions": { + "type": "nested", + "properties": { + "field": { + "type": "keyword" + }, + "text": { + "type": "keyword" + } + } + }, + "filterQuery": { + "properties": { + "expression": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "groupBy": { + "type": "nested", + "properties": { + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + } + }, + "legend": { + "properties": { + "palette": { + "type": "keyword" + }, + "reverseColors": { + "type": "boolean" + }, + "steps": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "nodeType": { + "type": "keyword" + }, + "region": { + "type": "keyword" + }, + "sort": { + "properties": { + "by": { + "type": "keyword" + }, + "direction": { + "type": "keyword" + } + } + }, + "time": { + "type": "long" + }, + "view": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "description": { + "type": "text" + }, + "expression": { + "type": "keyword", + "index": false + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "type": "object", + "enabled": false + }, + "metrics-explorer-view": { + "properties": { + "chartOptions": { + "properties": { + "stack": { + "type": "boolean" + }, + "type": { + "type": "keyword" + }, + "yAxisMode": { + "type": "keyword" + } + } + }, + "currentTimerange": { + "properties": { + "from": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "to": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "options": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "filterQuery": { + "type": "keyword" + }, + "forceInterval": { + "type": "boolean" + }, + "groupBy": { + "type": "keyword" + }, + "limit": { + "type": "integer" + }, + "metrics": { + "type": "nested", + "properties": { + "aggregation": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + } + }, + "source": { + "type": "keyword" + } + } + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "config": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "space": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "type": "object", + "enabled": false + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "type": "keyword", + "index": false + } + } + }, + "timefilter": { + "type": "object", + "enabled": false + }, + "title": { + "type": "text" + } + } + }, + "references": { + "type": "nested", + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "type": "keyword", + "index": false + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer", + "index": false + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text", + "index": false + } + } + }, + "sort": { + "type": "keyword", + "index": false + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "siem-detection-engine-rule-actions": { + "properties": { + "actions": { + "properties": { + "action_type_id": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "params": { + "type": "object", + "enabled": false + } + } + }, + "alertThrottle": { + "type": "keyword" + }, + "ruleAlertId": { + "type": "keyword" + }, + "ruleThrottle": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-status": { + "properties": { + "alertId": { + "type": "keyword" + }, + "bulkCreateTimeDurations": { + "type": "float" + }, + "gap": { + "type": "text" + }, + "lastFailureAt": { + "type": "date" + }, + "lastFailureMessage": { + "type": "text" + }, + "lastLookBackDate": { + "type": "date" + }, + "lastSuccessAt": { + "type": "date" + }, + "lastSuccessMessage": { + "type": "text" + }, + "searchAfterTimeDurations": { + "type": "float" + }, + "status": { + "type": "keyword" + }, + "statusDate": { + "type": "date" + } + } + }, + "siem-ui-timeline": { + "properties": { + "columns": { + "properties": { + "aggregatable": { + "type": "boolean" + }, + "category": { + "type": "keyword" + }, + "columnHeaderType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "example": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "indexes": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "placeholder": { + "type": "text" + }, + "searchable": { + "type": "boolean" + }, + "type": { + "type": "keyword" + } + } + }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "dataProviders": { + "properties": { + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" + } + } + }, + "description": { + "type": "text" + }, + "eventType": { + "type": "keyword" + }, + "excludedRowRendererIds": { + "type": "text" + }, + "favorite": { + "properties": { + "favoriteDate": { + "type": "date" + }, + "fullName": { + "type": "text" + }, + "keySearch": { + "type": "text" + }, + "userName": { + "type": "text" + } + } + }, + "filters": { + "properties": { + "exists": { + "type": "text" + }, + "match_all": { + "type": "text" + }, + "meta": { + "properties": { + "alias": { + "type": "text" + }, + "controlledBy": { + "type": "text" + }, + "disabled": { + "type": "boolean" + }, + "field": { + "type": "text" + }, + "formattedValue": { + "type": "text" + }, + "index": { + "type": "keyword" + }, + "key": { + "type": "keyword" + }, + "negate": { + "type": "boolean" + }, + "params": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "missing": { + "type": "text" + }, + "query": { + "type": "text" + }, + "range": { + "type": "text" + }, + "script": { + "type": "text" + } + } + }, + "kqlMode": { + "type": "keyword" + }, + "kqlQuery": { + "properties": { + "filterQuery": { + "properties": { + "kuery": { + "properties": { + "expression": { + "type": "text" + }, + "kind": { + "type": "keyword" + } + } + }, + "serializedQuery": { + "type": "text" + } + } + } + } + }, + "savedQueryId": { + "type": "keyword" + }, + "sort": { + "properties": { + "columnId": { + "type": "keyword" + }, + "sortDirection": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "templateTimelineId": { + "type": "text" + }, + "templateTimelineVersion": { + "type": "integer" + }, + "timelineType": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-note": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "type": "text", + "index": false + }, + "initials": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "properties": { + "errorMessage": { + "type": "keyword" + }, + "indexName": { + "type": "keyword" + }, + "lastCompletedStep": { + "type": "integer" + }, + "locked": { + "type": "date" + }, + "newIndexName": { + "type": "keyword" + }, + "reindexOptions": { + "properties": { + "openAndClose": { + "type": "boolean" + }, + "queueSettings": { + "properties": { + "queuedAt": { + "type": "long" + }, + "startedAt": { + "type": "long" + } + } + } + } + }, + "reindexTaskId": { + "type": "keyword" + }, + "reindexTaskPercComplete": { + "type": "float" + }, + "runningReindexCount": { + "type": "integer" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "type": "boolean", + "null_value": true + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "type": "long", + "null_value": 0 + }, + "indices": { + "type": "long", + "null_value": 0 + }, + "overview": { + "type": "long", + "null_value": 0 + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "type": "long", + "null_value": 0 + }, + "open": { + "type": "long", + "null_value": 0 + }, + "start": { + "type": "long", + "null_value": 0 + }, + "stop": { + "type": "long", + "null_value": 0 + } + } + } + } + }, + "uptime-dynamic-settings": { + "properties": { + "certAgeThreshold": { + "type": "long" + }, + "certExpirationThreshold": { + "type": "long" + }, + "heartbeatIndices": { + "type": "keyword" + } + } + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchRefName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + }, + "workplace_search_telemetry": { + "type": "object", + "dynamic": "false" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file From cbe8f007957b54f9a24029a613cbc3eb385bb2ca Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Tue, 14 Jul 2020 21:27:57 -0500 Subject: [PATCH 70/82] [Security Solution][Detections] Associate Endpoint Exceptions List to Rule during rule creation/update (#71794) * Add checkbox to associate rule with global endpoint exception list This works on creation, now we need edit. * Fix DomNesting error on ML Card Description EuiText generates a div, but this is inside of an EuiCard which is a paragraph. Defines a span with equivalent styles, instead. * Change default stack of alerts histogram to signal.rule.name --- .../components/alerts_histogram_panel/index.tsx | 2 +- .../select_rule_type/ml_card_description.tsx | 11 ++++++++--- .../rules/step_about_rule/default_value.ts | 1 + .../rules/step_about_rule/index.test.tsx | 2 ++ .../components/rules/step_about_rule/index.tsx | 16 ++++++++++++++-- .../components/rules/step_about_rule/schema.tsx | 10 ++++++++++ .../rules/step_about_rule/translations.ts | 8 ++++++++ .../detection_engine/rules/all/__mocks__/mock.ts | 1 + .../detection_engine/rules/create/helpers.ts | 8 ++++++++ .../detection_engine/rules/helpers.test.tsx | 4 +++- .../pages/detection_engine/rules/helpers.tsx | 2 ++ .../pages/detection_engine/rules/types.ts | 3 +++ 12 files changed, 61 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx index ba12499b8f20e..560c092d12076 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx @@ -83,7 +83,7 @@ const NO_LEGEND_DATA: LegendItem[] = []; export const AlertsHistogramPanel = memo( ({ chartHeight, - defaultStackByOption = alertsHistogramOptions[0], + defaultStackByOption = alertsHistogramOptions[8], // signal.rule.name deleteQuery, filters, headerChildren, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/ml_card_description.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/ml_card_description.tsx index 2171c93e47d63..79096c002f543 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/ml_card_description.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/ml_card_description.tsx @@ -5,7 +5,8 @@ */ import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiText, EuiLink } from '@elastic/eui'; +import { EuiLink } from '@elastic/eui'; +import styled from 'styled-components'; import React from 'react'; import { ML_TYPE_DESCRIPTION } from './translations'; @@ -15,11 +16,15 @@ interface MlCardDescriptionProps { hasValidLicense?: boolean; } +const SmallText = styled.span` + font-size: ${({ theme }) => theme.eui.euiFontSizeS}; +`; + const MlCardDescriptionComponent: React.FC = ({ subscriptionUrl, hasValidLicense = false, }) => ( - + {hasValidLicense ? ( ML_TYPE_DESCRIPTION ) : ( @@ -38,7 +43,7 @@ const MlCardDescriptionComponent: React.FC = ({ }} /> )} - + ); MlCardDescriptionComponent.displayName = 'MlCardDescriptionComponent'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts index 060a2183eb06e..f5d61553b595b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts @@ -18,6 +18,7 @@ export const stepAboutDefaultValue: AboutStepRule = { author: [], name: '', description: '', + isAssociatedToEndpointList: false, isBuildingBlock: false, isNew: true, severity: { value: 'low', mapping: [] }, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx index b21c54a0b6131..9b2e0069f0ac0 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx @@ -165,6 +165,7 @@ describe('StepAboutRuleComponent', () => { await wait(); const expected: Omit = { author: [], + isAssociatedToEndpointList: false, isBuildingBlock: false, license: '', ruleNameOverride: '', @@ -223,6 +224,7 @@ describe('StepAboutRuleComponent', () => { await wait(); const expected: Omit = { author: [], + isAssociatedToEndpointList: false, isBuildingBlock: false, license: '', ruleNameOverride: '', diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx index 3616643874a0a..4d91460bfd2c8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx @@ -282,7 +282,20 @@ const StepAboutRuleComponent: FC = ({ }} /> - + + + + = ({ euiFieldProps: { fullWidth: true, isDisabled: isLoading, - placeholder: '', }, }} /> diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx index 309557e5c9421..f178923df5915 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx @@ -91,6 +91,16 @@ export const schema: FormSchema = { ), labelAppend: OptionalFieldLabel, }, + isAssociatedToEndpointList: { + type: FIELD_TYPES.CHECKBOX, + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldAssociatedToEndpointListLabel', + { + defaultMessage: 'Associate rule to Global Endpoint Exception List', + } + ), + labelAppend: OptionalFieldLabel, + }, severity: { value: { type: FIELD_TYPES.SUPER_SELECT, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts index 3a5aa3c56c3df..939747717385c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts @@ -26,6 +26,14 @@ export const ADD_FALSE_POSITIVE = i18n.translate( defaultMessage: 'Add false positive example', } ); + +export const GLOBAL_ENDPOINT_EXCEPTION_LIST = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.endpointExceptionListLabel', + { + defaultMessage: 'Global endpoint exception list', + } +); + export const BUILDING_BLOCK = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.buildingBlockLabel', { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts index 5d84cf5314029..10d969ae7e6e8 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts @@ -167,6 +167,7 @@ export const mockRuleWithEverything = (id: string): Rule => ({ export const mockAboutStepRule = (isNew = false): AboutStepRule => ({ isNew, author: ['Elastic'], + isAssociatedToEndpointList: false, isBuildingBlock: false, timestampOverride: '', ruleNameOverride: '', diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index c419dd142cfbe..226fa5313e34f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -153,6 +153,7 @@ export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRule riskScore, severity, threat, + isAssociatedToEndpointList, isBuildingBlock, isNew, note, @@ -163,6 +164,13 @@ export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRule const resp = { author: author.filter((item) => !isEmpty(item)), ...(isBuildingBlock ? { building_block_type: 'default' } : {}), + ...(isAssociatedToEndpointList + ? { + exceptions_list: [ + { id: 'endpoint_list', namespace_type: 'agnostic', type: 'endpoint' }, + ] as AboutStepRuleJson['exceptions_list'], + } + : {}), false_positives: falsePositives.filter((item) => !isEmpty(item)), references: references.filter((item) => !isEmpty(item)), risk_score: riskScore.value, 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 590643f8236ee..c01317e4f48c5 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 @@ -83,10 +83,12 @@ describe('rule helpers', () => { title: 'Titled timeline', }, }; - const aboutRuleStepData = { + + const aboutRuleStepData: AboutStepRule = { author: [], description: '24/7', falsePositives: ['test'], + isAssociatedToEndpointList: false, isBuildingBlock: false, isNew: false, license: 'Elastic License', 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 6541b92f575c1..5df711ea7cd8e 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 @@ -122,6 +122,7 @@ export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRu const { author, building_block_type: buildingBlockType, + exceptions_list: exceptionsList, license, risk_score_mapping: riskScoreMapping, rule_name_override: ruleNameOverride, @@ -138,6 +139,7 @@ export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRu return { isNew: false, author, + isAssociatedToEndpointList: exceptionsList?.some(({ id }) => id === 'endpoint_list') ?? false, isBuildingBlock: buildingBlockType !== undefined, license: license ?? '', ruleNameOverride: ruleNameOverride ?? '', 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 b501536e5b387..23715a88efc7b 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 @@ -20,6 +20,7 @@ import { SeverityMapping, TimestampOverride, } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { List } from '../../../../../common/detection_engine/schemas/types'; export interface EuiBasicTableSortTypes { field: string; @@ -65,6 +66,7 @@ export interface AboutStepRule extends StepRuleData { author: string[]; name: string; description: string; + isAssociatedToEndpointList: boolean; isBuildingBlock: boolean; severity: AboutStepSeverity; riskScore: AboutStepRiskScore; @@ -136,6 +138,7 @@ export interface DefineStepRuleJson { export interface AboutStepRuleJson { author: Author; building_block_type?: BuildingBlockType; + exceptions_list?: List[]; name: string; description: string; license: License; From a8513256a00f7d526e396b22707a7536a2bb38a0 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 14 Jul 2020 19:43:44 -0700 Subject: [PATCH 71/82] [test] Skipped monitoring test Signed-off-by: Tyler Smalley --- x-pack/test/functional/apps/monitoring/cluster/overview.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/monitoring/cluster/overview.js b/x-pack/test/functional/apps/monitoring/cluster/overview.js index 0e608e9a055fa..94996d6ab40ab 100644 --- a/x-pack/test/functional/apps/monitoring/cluster/overview.js +++ b/x-pack/test/functional/apps/monitoring/cluster/overview.js @@ -10,7 +10,8 @@ import { getLifecycleMethods } from '../_get_lifecycle_methods'; export default function ({ getService, getPageObjects }) { const overview = getService('monitoringClusterOverview'); - describe('Cluster overview', () => { + // https://github.com/elastic/kibana/issues/71796 + describe.skip('Cluster overview', () => { describe('for Green cluster with Gold license', () => { const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects); From 3984ffa13530d9486552c91497b9aef4c2be0e9f Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 14 Jul 2020 19:54:32 -0700 Subject: [PATCH 72/82] [tests] Temporarily skipped Fleet tests Most fleet tests are colliding with the change to timestamp_field ES change https://github.com/elastic/kibana/pull/71727 Signed-off-by: Tyler Smalley --- x-pack/test/api_integration/apis/fleet/agent_flow.ts | 2 +- x-pack/test/api_integration/apis/fleet/agents/enroll.ts | 4 +--- x-pack/test/api_integration/apis/fleet/index.js | 4 +++- x-pack/test/api_integration/apis/fleet/setup.ts | 4 +--- x-pack/test/api_integration/apis/fleet/unenroll_agent.ts | 4 +--- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/x-pack/test/api_integration/apis/fleet/agent_flow.ts b/x-pack/test/api_integration/apis/fleet/agent_flow.ts index e14a85d6e30c1..da472ca912d40 100644 --- a/x-pack/test/api_integration/apis/fleet/agent_flow.ts +++ b/x-pack/test/api_integration/apis/fleet/agent_flow.ts @@ -18,7 +18,7 @@ export default function (providerContext: FtrProviderContext) { const supertestWithoutAuth = getSupertestWithoutAuth(providerContext); const esClient = getService('es'); - describe.skip('fleet_agent_flow', () => { + describe('fleet_agent_flow', () => { before(async () => { await esArchiver.load('empty_kibana'); }); diff --git a/x-pack/test/api_integration/apis/fleet/agents/enroll.ts b/x-pack/test/api_integration/apis/fleet/agents/enroll.ts index d83b648fce0a9..e9f7471f6437e 100644 --- a/x-pack/test/api_integration/apis/fleet/agents/enroll.ts +++ b/x-pack/test/api_integration/apis/fleet/agents/enroll.ts @@ -21,9 +21,7 @@ export default function (providerContext: FtrProviderContext) { let apiKey: { id: string; api_key: string }; let kibanaVersion: string; - // Temporarily skipped to promote snapshot - // Re-enabled in https://github.com/elastic/kibana/pull/71727 - describe.skip('fleet_agents_enroll', () => { + describe('fleet_agents_enroll', () => { before(async () => { await esArchiver.loadIfNeeded('fleet/agents'); diff --git a/x-pack/test/api_integration/apis/fleet/index.js b/x-pack/test/api_integration/apis/fleet/index.js index df81b826132a9..ec80b9aed4be0 100644 --- a/x-pack/test/api_integration/apis/fleet/index.js +++ b/x-pack/test/api_integration/apis/fleet/index.js @@ -5,7 +5,9 @@ */ export default function loadTests({ loadTestFile }) { - describe('Fleet Endpoints', () => { + // Temporarily skipped to promote snapshot + // Re-enabled in https://github.com/elastic/kibana/pull/71727 + describe.skip('Fleet Endpoints', () => { loadTestFile(require.resolve('./setup')); loadTestFile(require.resolve('./delete_agent')); loadTestFile(require.resolve('./list_agent')); diff --git a/x-pack/test/api_integration/apis/fleet/setup.ts b/x-pack/test/api_integration/apis/fleet/setup.ts index 317dec734568c..4fcf39886e202 100644 --- a/x-pack/test/api_integration/apis/fleet/setup.ts +++ b/x-pack/test/api_integration/apis/fleet/setup.ts @@ -11,9 +11,7 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const es = getService('es'); - // Temporarily skipped to promote snapshot - // Re-enabled in https://github.com/elastic/kibana/pull/71727 - describe.skip('fleet_setup', () => { + describe('fleet_setup', () => { beforeEach(async () => { try { await es.security.deleteUser({ diff --git a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts index 76cd48b63e869..bc6c44e590cc4 100644 --- a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts +++ b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts @@ -16,9 +16,7 @@ export default function (providerContext: FtrProviderContext) { const supertest = getService('supertest'); const esClient = getService('es'); - // Temporarily skipped to promote snapshot - // Re-enabled in https://github.com/elastic/kibana/pull/71727 - describe.skip('fleet_unenroll_agent', () => { + describe('fleet_unenroll_agent', () => { let accessAPIKeyId: string; let outputAPIKeyId: string; before(async () => { From 3c8a66e2b3be56ff247231174c7c2c9b8c7cee66 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 14 Jul 2020 21:01:19 -0700 Subject: [PATCH 73/82] Revert "re-fix navigate path for master add SAML login to login_page (#71337)" This reverts commit 1f340969eeb2a5f977e1bad28daab5f2fb96a3a0. --- test/functional/page_objects/login_page.ts | 60 ++----------------- ...onfig.stack_functional_integration_base.js | 8 +-- .../functional/apps/sample_data/e_commerce.js | 2 +- 3 files changed, 8 insertions(+), 62 deletions(-) diff --git a/test/functional/page_objects/login_page.ts b/test/functional/page_objects/login_page.ts index 350ab8be1a274..c84f47a342155 100644 --- a/test/functional/page_objects/login_page.ts +++ b/test/functional/page_objects/login_page.ts @@ -7,76 +7,26 @@ * not use this file except in compliance with the License. * You may obtain a copy of the License at * - *    http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied.  See the License for the + * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ -import { delay } from 'bluebird'; import { FtrProviderContext } from '../ftr_provider_context'; export function LoginPageProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); - const log = getService('log'); - const find = getService('find'); - - const regularLogin = async (user: string, pwd: string) => { - await testSubjects.setValue('loginUsername', user); - await testSubjects.setValue('loginPassword', pwd); - await testSubjects.click('loginSubmit'); - await find.waitForDeletedByCssSelector('.kibanaWelcomeLogo'); - await find.byCssSelector('[data-test-subj="kibanaChrome"]', 60000); // 60 sec waiting - }; - - const samlLogin = async (user: string, pwd: string) => { - try { - await find.clickByButtonText('Login using SAML'); - await find.setValue('input[name="email"]', user); - await find.setValue('input[type="password"]', pwd); - await find.clickByCssSelector('.auth0-label-submit'); - await find.byCssSelector('[data-test-subj="kibanaChrome"]', 60000); // 60 sec waiting - } catch (err) { - log.debug(`${err} \nFailed to find Auth0 login page, trying the Auth0 last login page`); - await find.clickByCssSelector('.auth0-lock-social-button'); - } - }; class LoginPage { async login(user: string, pwd: string) { - if ( - process.env.VM === 'ubuntu18_deb_oidc' || - process.env.VM === 'ubuntu16_deb_desktop_saml' - ) { - await samlLogin(user, pwd); - return; - } - - await regularLogin(user, pwd); - } - - async logoutLogin(user: string, pwd: string) { - await this.logout(); - await this.sleep(3002); - await this.login(user, pwd); - } - - async logout() { - await testSubjects.click('userMenuButton'); - await this.sleep(500); - await testSubjects.click('logoutLink'); - log.debug('### found and clicked log out--------------------------'); - await this.sleep(8002); - } - - async sleep(sleepMilliseconds: number) { - log.debug(`... sleep(${sleepMilliseconds}) start`); - await delay(sleepMilliseconds); - log.debug(`... sleep(${sleepMilliseconds}) end`); + await testSubjects.setValue('loginUsername', user); + await testSubjects.setValue('loginPassword', pwd); + await testSubjects.click('loginSubmit'); } } diff --git a/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js b/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js index 96d338a04b01b..a34d158496ba0 100644 --- a/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js +++ b/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js @@ -12,16 +12,12 @@ import { esTestConfig, kbnTestConfig } from '@kbn/test'; const reportName = 'Stack Functional Integration Tests'; const testsFolder = '../test/functional/apps'; +const stateFilePath = '../../../../../integration-test/qa/envvars.sh'; +const prepend = (testFile) => require.resolve(`${testsFolder}/${testFile}`); const log = new ToolingLog({ level: 'info', writeTo: process.stdout, }); -log.info(`WORKSPACE in config file ${process.env.WORKSPACE}`); -const stateFilePath = process.env.WORKSPACE - ? `${process.env.WORKSPACE}/qa/envvars.sh` - : '../../../../../integration-test/qa/envvars.sh'; - -const prepend = (testFile) => require.resolve(`${testsFolder}/${testFile}`); export default async ({ readConfigFile }) => { const defaultConfigs = await readConfigFile(require.resolve('../../functional/config')); diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/sample_data/e_commerce.js b/x-pack/test/stack_functional_integration/test/functional/apps/sample_data/e_commerce.js index 0286f6984e89e..306f30133f6ee 100644 --- a/x-pack/test/stack_functional_integration/test/functional/apps/sample_data/e_commerce.js +++ b/x-pack/test/stack_functional_integration/test/functional/apps/sample_data/e_commerce.js @@ -12,7 +12,7 @@ export default function ({ getService, getPageObjects }) { before(async () => { await browser.setWindowSize(1200, 800); - await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + await PageObjects.common.navigateToUrl('home', '/home/tutorial_directory/sampleData', { useActualUrl: true, insertTimestamp: false, }); From ddbfe53e2271ba7af27e3785cf7f3466b430b54f Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 14 Jul 2020 23:36:05 -0700 Subject: [PATCH 74/82] [test] Skips flaky detection engine tests https://github.com/elastic/kibana/issues/71814 Signed-off-by: Tyler Smalley --- .../integration/alerts_detection_rules_prebuilt.spec.ts | 3 ++- .../security_and_spaces/tests/add_prepackaged_rules.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_prebuilt.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_prebuilt.spec.ts index 986a7c7177a79..00ddc85a73650 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_prebuilt.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_prebuilt.spec.ts @@ -67,7 +67,8 @@ describe('Alerts rules, prebuilt rules', () => { }); }); -describe('Deleting prebuilt rules', () => { +// https://github.com/elastic/kibana/issues/71814 +describe.skip('Deleting prebuilt rules', () => { beforeEach(() => { const expectedNumberOfRules = totalNumberOfPrebuiltRules; const expectedElasticRulesBtnText = `Elastic rules (${expectedNumberOfRules})`; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts index 242f906d0d197..5e0ce0b824323 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts @@ -20,7 +20,8 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); - describe('add_prepackaged_rules', () => { + // https://github.com/elastic/kibana/issues/71814 + describe.skip('add_prepackaged_rules', () => { describe('validation errors', () => { it('should give an error that the index must exist first if it does not exist before adding prepackaged rules', async () => { const { body } = await supertest From 6868ece76620336d1cd7ae408acc096f1525bbc8 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 14 Jul 2020 23:40:35 -0700 Subject: [PATCH 75/82] [test] Skips Ingest Manager test preventing ES promotion Signed-off-by: Tyler Smalley --- x-pack/test/ingest_manager_api_integration/apis/epm/install.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install.ts index 54a7e0dcb9242..f2ca98ca39a0b 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/install.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install.ts @@ -23,7 +23,7 @@ export default function ({ getService }: FtrProviderContext) { // Temporarily skipped to promote snapshot // Re-enabled in https://github.com/elastic/kibana/pull/71727 - describe('installs packages that include settings and mappings overrides', async () => { + describe.skip('installs packages that include settings and mappings overrides', async () => { after(async () => { if (server.enabled) { // remove the package just in case it being installed will affect other tests From 51a862988c344b34bd9da57dd57008df12e1b5e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Wed, 15 Jul 2020 08:41:57 +0200 Subject: [PATCH 76/82] [APM] Increase `xpack.apm.ui.transactionGroupBucketSize` (#71661) --- docs/settings/apm-settings.asciidoc | 2 +- x-pack/plugins/apm/server/index.ts | 2 +- .../lib/transaction_groups/__snapshots__/fetcher.test.ts.snap | 2 +- .../lib/transaction_groups/__snapshots__/queries.test.ts.snap | 2 +- x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts | 4 +++- .../tests/services/transactions/top_transaction_groups.ts | 2 +- .../test/apm_api_integration/basic/tests/traces/top_traces.ts | 2 +- 7 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index f78b0642f7fa3..b396c40aa21f9 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -47,7 +47,7 @@ Changing these settings may disable features of the APM App. | Set to `false` to hide the APM app from the menu. Defaults to `true`. | `xpack.apm.ui.transactionGroupBucketSize` - | Number of top transaction groups displayed in the APM app. Defaults to `100`. + | Number of top transaction groups displayed in the APM app. Defaults to `1000`. | `xpack.apm.ui.maxTraceItems` {ess-icon} | Maximum number of child items displayed when viewing trace details. Defaults to `1000`. diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index 74494985fba0b..431210926c948 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -27,7 +27,7 @@ export const config = { autocreateApmIndexPattern: schema.boolean({ defaultValue: true }), ui: schema.object({ enabled: schema.boolean({ defaultValue: true }), - transactionGroupBucketSize: schema.number({ defaultValue: 100 }), + transactionGroupBucketSize: schema.number({ defaultValue: 1000 }), maxTraceItems: schema.number({ defaultValue: 1000 }), }), telemetryCollectionEnabled: schema.boolean({ defaultValue: true }), diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap index 087dc6afc9a58..b354d3ed1f88d 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap @@ -46,7 +46,7 @@ Array [ }, }, "composite": Object { - "size": 101, + "size": 10000, "sources": Array [ Object { "service": Object { diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap index 496533cf97e65..884a7d18cc4d4 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap @@ -44,7 +44,7 @@ Object { }, }, "composite": Object { - "size": 101, + "size": 10000, "sources": Array [ Object { "service": Object { diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index 595ee9d8da2dc..a5cc74b18a7ef 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -72,7 +72,9 @@ export async function transactionGroupsFetcher( aggs: { transaction_groups: { composite: { - size: bucketSize + 1, // 1 extra bucket is added to check whether the total number of buckets exceed the specified bucket size. + // traces overview is hardcoded to 10000 + // transactions overview: 1 extra bucket is added to check whether the total number of buckets exceed the specified bucket size. + size: isTopTraces ? 10000 : bucketSize + 1, sources: [ ...(isTopTraces ? [{ service: { terms: { field: SERVICE_NAME } } }] diff --git a/x-pack/test/apm_api_integration/basic/tests/services/transactions/top_transaction_groups.ts b/x-pack/test/apm_api_integration/basic/tests/services/transactions/top_transaction_groups.ts index 3df1e9972d5ac..bf8d3f6a56e6a 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/transactions/top_transaction_groups.ts +++ b/x-pack/test/apm_api_integration/basic/tests/services/transactions/top_transaction_groups.ts @@ -25,7 +25,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); expect(response.status).to.be(200); - expect(response.body).to.eql({ items: [], isAggregationAccurate: true, bucketSize: 100 }); + expect(response.body).to.eql({ items: [], isAggregationAccurate: true, bucketSize: 1000 }); }); }); diff --git a/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts b/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts index ca50ae291f110..aef208b6fc06b 100644 --- a/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts +++ b/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts @@ -24,7 +24,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); expect(response.status).to.be(200); - expect(response.body).to.eql({ items: [], isAggregationAccurate: true, bucketSize: 100 }); + expect(response.body).to.eql({ items: [], isAggregationAccurate: true, bucketSize: 1000 }); }); }); From f760d8513b0216a73e9a476661f0fb8fb0887a61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Wed, 15 Jul 2020 08:42:17 +0200 Subject: [PATCH 77/82] [APM] Remove watcher integration (#71655) --- .../ServiceIntegrations/WatcherFlyout.tsx | 635 ------------------ .../createErrorGroupWatch.test.ts.snap | 169 ----- .../__test__/createErrorGroupWatch.test.ts | 120 ---- .../__test__/esResponse.ts | 149 ---- .../createErrorGroupWatch.ts | 261 ------- .../ServiceIntegrations/index.tsx | 122 ---- .../components/app/ServiceDetails/index.tsx | 4 - .../apm/public/services/rest/watcher.ts | 24 - .../translations/translations/ja-JP.json | 37 - .../translations/translations/zh-CN.json | 37 - 10 files changed, 1558 deletions(-) delete mode 100644 x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx delete mode 100644 x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/__snapshots__/createErrorGroupWatch.test.ts.snap delete mode 100644 x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts delete mode 100644 x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/esResponse.ts delete mode 100644 x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts delete mode 100644 x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx delete mode 100644 x-pack/plugins/apm/public/services/rest/watcher.ts diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx deleted file mode 100644 index 26cff5e71b610..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx +++ /dev/null @@ -1,635 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - EuiButton, - EuiFieldNumber, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiForm, - EuiFormRow, - EuiLink, - EuiRadio, - EuiSelect, - EuiSpacer, - EuiSwitch, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { padStart, range } from 'lodash'; -import moment from 'moment-timezone'; -import React, { Component } from 'react'; -import styled from 'styled-components'; -import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; -import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import { KibanaLink } from '../../../shared/Links/KibanaLink'; -import { createErrorGroupWatch, Schedule } from './createErrorGroupWatch'; -import { ElasticDocsLink } from '../../../shared/Links/ElasticDocsLink'; -import { ApmPluginContext } from '../../../../context/ApmPluginContext'; -import { getApmIndexPatternTitle } from '../../../../services/rest/index_pattern'; - -type ScheduleKey = keyof Schedule; - -const SmallInput = styled.div` - .euiFormRow { - max-width: 85px; - } - .euiFormHelpText { - width: 200px; - } -`; - -interface WatcherFlyoutProps { - urlParams: IUrlParams; - onClose: () => void; - isOpen: boolean; -} - -type IntervalUnit = 'm' | 'h'; - -interface WatcherFlyoutState { - schedule: ScheduleKey; - threshold: number; - actions: { - slack: boolean; - email: boolean; - }; - interval: { - value: number; - unit: IntervalUnit; - }; - daily: string; - emails: string; - slackUrl: string; -} - -export class WatcherFlyout extends Component< - WatcherFlyoutProps, - WatcherFlyoutState -> { - static contextType = ApmPluginContext; - context!: React.ContextType; - public state: WatcherFlyoutState = { - schedule: 'daily', - threshold: 10, - actions: { - slack: false, - email: false, - }, - interval: { - value: 10, - unit: 'm', - }, - daily: '08:00', - emails: '', - slackUrl: '', - }; - - public onChangeSchedule = (schedule: ScheduleKey) => { - this.setState({ schedule }); - }; - - public onChangeThreshold = (event: React.ChangeEvent) => { - this.setState({ - threshold: parseInt(event.target.value, 10), - }); - }; - - public onChangeDailyUnit = (event: React.ChangeEvent) => { - this.setState({ - daily: event.target.value, - }); - }; - - public onChangeIntervalValue = ( - event: React.ChangeEvent - ) => { - this.setState({ - interval: { - value: parseInt(event.target.value, 10), - unit: this.state.interval.unit, - }, - }); - }; - - public onChangeIntervalUnit = ( - event: React.ChangeEvent - ) => { - this.setState({ - interval: { - value: this.state.interval.value, - unit: event.target.value as IntervalUnit, - }, - }); - }; - - public onChangeAction = (actionName: 'slack' | 'email') => { - this.setState({ - actions: { - ...this.state.actions, - [actionName]: !this.state.actions[actionName], - }, - }); - }; - - public onChangeEmails = (event: React.ChangeEvent) => { - this.setState({ emails: event.target.value }); - }; - - public onChangeSlackUrl = (event: React.ChangeEvent) => { - this.setState({ slackUrl: event.target.value }); - }; - - public createWatch = () => { - const { serviceName } = this.props.urlParams; - const { core } = this.context; - - if (!serviceName) { - return; - } - - const emails = this.state.actions.email - ? this.state.emails - .split(',') - .map((email) => email.trim()) - .filter((email) => !!email) - : []; - - const slackUrl = this.state.actions.slack ? this.state.slackUrl : ''; - - const schedule = - this.state.schedule === 'interval' - ? { - interval: `${this.state.interval.value}${this.state.interval.unit}`, - } - : { - daily: { at: `${this.state.daily}` }, - }; - - const timeRange = - this.state.schedule === 'interval' - ? { - value: this.state.interval.value, - unit: this.state.interval.unit, - } - : { - value: 24, - unit: 'h', - }; - - return getApmIndexPatternTitle() - .then((indexPatternTitle) => { - return createErrorGroupWatch({ - http: core.http, - emails, - schedule, - serviceName, - slackUrl, - threshold: this.state.threshold, - timeRange, - apmIndexPatternTitle: indexPatternTitle, - }).then((id: string) => { - this.props.onClose(); - this.addSuccessToast(id); - }); - }) - .catch((e) => { - // eslint-disable-next-line - console.error(e); - this.addErrorToast(); - }); - }; - - public addErrorToast = () => { - const { core } = this.context; - - core.notifications.toasts.addWarning({ - title: i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreationFailedNotificationTitle', - { - defaultMessage: 'Watch creation failed', - } - ), - text: toMountPoint( -

- {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreationFailedNotificationText', - { - defaultMessage: - 'Make sure your user has permission to create watches.', - } - )} -

- ), - }); - }; - - public addSuccessToast = (id: string) => { - const { core } = this.context; - - core.notifications.toasts.addSuccess({ - title: i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreatedNotificationTitle', - { - defaultMessage: 'New watch created!', - } - ), - text: toMountPoint( -

- {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreatedNotificationText', - { - defaultMessage: - 'The watch is now ready and will send error reports for {serviceName}.', - values: { - serviceName: this.props.urlParams.serviceName, - }, - } - )}{' '} - - - {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreatedNotificationText.viewWatchLinkText', - { - defaultMessage: 'View watch', - } - )} - - -

- ), - }); - }; - - public render() { - if (!this.props.isOpen) { - return null; - } - - const dailyTime = this.state.daily; - const inputTime = `${dailyTime}Z`; // Add tz to make into UTC - const inputFormat = 'HH:mmZ'; // Parse as 24 hour w. tz - const dailyTimeFormatted = moment(inputTime, inputFormat).format('HH:mm'); // Format as 24h - const dailyTime12HourFormatted = moment(inputTime, inputFormat).format( - 'hh:mm A (z)' - ); // Format as 12h w. tz - - // Generate UTC hours for Daily Report select field - const intervalHours = range(24).map((i) => { - const hour = padStart(i.toString(), 2, '0'); - return { value: `${hour}:00`, text: `${hour}:00 UTC` }; - }); - - const flyoutBody = ( - -

- - {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.formDescription.documentationLinkText', - { - defaultMessage: 'documentation', - } - )} - - ), - }} - /> -

- - -

- {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.conditionTitle', - { - defaultMessage: 'Condition', - } - )} -

- - - - -

- {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.triggerScheduleTitle', - { - defaultMessage: 'Trigger schedule', - } - )} -

- - {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.triggerScheduleDescription', - { - defaultMessage: - 'Choose the time interval for the report, when the threshold is exceeded.', - } - )} - - - this.onChangeSchedule('daily')} - checked={this.state.schedule === 'daily'} - /> - - - - - - this.onChangeSchedule('interval')} - checked={this.state.schedule === 'interval'} - /> - - - - - - - - - - - - - - - -

- {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.actionsTitle', - { - defaultMessage: 'Actions', - } - )} -

- - {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.actionsDescription', - { - defaultMessage: - 'Reports can be sent by email or posted to a Slack channel. Each report will include the top 10 errors sorted by occurrence.', - } - )} - - - this.onChangeAction('email')} - /> - - {this.state.actions.email && ( - - - {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.recipientsHelpText.documentationLinkText', - { - defaultMessage: 'documentation', - } - )} - - ), - }} - /> - - } - > - - - )} - - this.onChangeAction('slack')} - /> - - {this.state.actions.slack && ( - - - {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.slackWebhookURLHelpText.documentationLinkText', - { - defaultMessage: 'documentation', - } - )} - - ), - }} - /> - - } - > - - - )} -
-
- ); - - return ( - - - -

- {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.enableErrorReportsTitle', - { - defaultMessage: 'Enable error reports', - } - )} -

-
-
- {flyoutBody} - - - - this.createWatch()} - fill - disabled={ - !this.state.actions.email && !this.state.actions.slack - } - > - {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.createWatchButtonLabel', - { - defaultMessage: 'Create watch', - } - )} - - - - -
- ); - } -} diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/__snapshots__/createErrorGroupWatch.test.ts.snap b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/__snapshots__/createErrorGroupWatch.test.ts.snap deleted file mode 100644 index 88f254747c686..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/__snapshots__/createErrorGroupWatch.test.ts.snap +++ /dev/null @@ -1,169 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`createErrorGroupWatch should format email correctly 1`] = ` -"Your service \\"opbeans-node\\" has error groups which exceeds 10 occurrences within \\"24h\\" - - -this is a string -N/A -7761 occurrences - -foo - (server/coffee.js) -7752 occurrences - -socket hang up -createHangUpError (_http_client.js) -3887 occurrences - -this will not get captured by express - (server/coffee.js) -3886 occurrences -" -`; - -exports[`createErrorGroupWatch should format slack message correctly 1`] = ` -"Your service \\"opbeans-node\\" has error groups which exceeds 10 occurrences within \\"24h\\" - ->*this is a string* ->N/A ->7761 occurrences - ->*foo* ->\` (server/coffee.js)\` ->7752 occurrences - ->*socket hang up* ->\`createHangUpError (_http_client.js)\` ->3887 occurrences - ->*this will not get captured by express* ->\` (server/coffee.js)\` ->3886 occurrences -" -`; - -exports[`createErrorGroupWatch should format template correctly 1`] = ` -Object { - "actions": Object { - "email": Object { - "email": Object { - "body": Object { - "html": "Your service \\"opbeans-node\\" has error groups which exceeds 10 occurrences within \\"24h\\"


this is a string
N/A
7761 occurrences

foo
(server/coffee.js)
7752 occurrences

socket hang up
createHangUpError (_http_client.js)
3887 occurrences

this will not get captured by express
(server/coffee.js)
3886 occurrences
", - }, - "subject": "\\"opbeans-node\\" has error groups which exceeds the threshold", - "to": "my@email.dk,mySecond@email.dk", - }, - }, - "log_error": Object { - "logging": Object { - "text": "Your service \\"opbeans-node\\" has error groups which exceeds 10 occurrences within \\"24h\\"


this is a string
N/A
7761 occurrences

foo
(server/coffee.js)
7752 occurrences

socket hang up
createHangUpError (_http_client.js)
3887 occurrences

this will not get captured by express
(server/coffee.js)
3886 occurrences
", - }, - }, - "slack_webhook": Object { - "webhook": Object { - "body": "__json__::{\\"text\\":\\"Your service \\\\\\"opbeans-node\\\\\\" has error groups which exceeds 10 occurrences within \\\\\\"24h\\\\\\"\\\\n\\\\n>*this is a string*\\\\n>N/A\\\\n>7761 occurrences\\\\n\\\\n>*foo*\\\\n>\` (server/coffee.js)\`\\\\n>7752 occurrences\\\\n\\\\n>*socket hang up*\\\\n>\`createHangUpError (_http_client.js)\`\\\\n>3887 occurrences\\\\n\\\\n>*this will not get captured by express*\\\\n>\` (server/coffee.js)\`\\\\n>3886 occurrences\\\\n\\"}", - "headers": Object { - "Content-Type": "application/json", - }, - "host": "hooks.slack.com", - "method": "POST", - "path": "/services/slackid1/slackid2/slackid3", - "port": 443, - "scheme": "https", - }, - }, - }, - "condition": Object { - "script": Object { - "source": "return ctx.payload.aggregations.error_groups.buckets.length > 0", - }, - }, - "input": Object { - "search": Object { - "request": Object { - "body": Object { - "aggs": Object { - "error_groups": Object { - "aggs": Object { - "sample": Object { - "top_hits": Object { - "_source": Array [ - "error.log.message", - "error.exception.message", - "error.exception.handled", - "error.culprit", - "error.grouping_key", - "@timestamp", - ], - "size": 1, - "sort": Array [ - Object { - "@timestamp": "desc", - }, - ], - }, - }, - }, - "terms": Object { - "field": "error.grouping_key", - "min_doc_count": "10", - "order": Object { - "_count": "desc", - }, - "size": 10, - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "term": Object { - "service.name": "opbeans-node", - }, - }, - Object { - "term": Object { - "processor.event": "error", - }, - }, - Object { - "range": Object { - "@timestamp": Object { - "gte": "now-24h", - }, - }, - }, - ], - }, - }, - "size": 0, - }, - "indices": Array [ - "myIndexPattern", - ], - }, - }, - }, - "metadata": Object { - "emails": Array [ - "my@email.dk", - "mySecond@email.dk", - ], - "serviceName": "opbeans-node", - "slackUrlPath": "/services/slackid1/slackid2/slackid3", - "threshold": 10, - "timeRangeUnit": "h", - "timeRangeValue": 24, - "trigger": "This value must be changed in trigger section", - }, - "trigger": Object { - "schedule": Object { - "daily": Object { - "at": "08:00", - }, - }, - }, -} -`; diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts deleted file mode 100644 index 054476af28de1..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isArray, isObject, isString } from 'lodash'; -import mustache from 'mustache'; -import uuid from 'uuid'; -import * as rest from '../../../../../services/rest/watcher'; -import { createErrorGroupWatch } from '../createErrorGroupWatch'; -import { esResponse } from './esResponse'; -import { HttpSetup } from 'kibana/public'; - -// disable html escaping since this is also disabled in watcher\s mustache implementation -mustache.escape = (value) => value; - -jest.mock('../../../../../services/rest/callApi', () => ({ - callApi: () => Promise.resolve(null), -})); - -describe('createErrorGroupWatch', () => { - let createWatchResponse: string; - let tmpl: any; - const createWatchSpy = jest - .spyOn(rest, 'createWatch') - .mockResolvedValue(undefined); - - beforeEach(async () => { - jest.spyOn(uuid, 'v4').mockReturnValue(Buffer.from('mocked-uuid')); - - createWatchResponse = await createErrorGroupWatch({ - http: {} as HttpSetup, - emails: ['my@email.dk', 'mySecond@email.dk'], - schedule: { - daily: { - at: '08:00', - }, - }, - serviceName: 'opbeans-node', - slackUrl: 'https://hooks.slack.com/services/slackid1/slackid2/slackid3', - threshold: 10, - timeRange: { value: 24, unit: 'h' }, - apmIndexPatternTitle: 'myIndexPattern', - }); - - const watchBody = createWatchSpy.mock.calls[0][0].watch; - const templateCtx = { - payload: esResponse, - metadata: watchBody.metadata, - }; - - tmpl = renderMustache(createWatchSpy.mock.calls[0][0].watch, templateCtx); - }); - - afterEach(() => jest.restoreAllMocks()); - - it('should call createWatch with correct args', () => { - expect(createWatchSpy.mock.calls[0][0].id).toBe('apm-mocked-uuid'); - }); - - it('should format slack message correctly', () => { - expect(tmpl.actions.slack_webhook.webhook.path).toBe( - '/services/slackid1/slackid2/slackid3' - ); - - expect( - JSON.parse(tmpl.actions.slack_webhook.webhook.body.slice(10)).text - ).toMatchSnapshot(); - }); - - it('should format email correctly', () => { - expect(tmpl.actions.email.email.to).toEqual( - 'my@email.dk,mySecond@email.dk' - ); - expect(tmpl.actions.email.email.subject).toBe( - '"opbeans-node" has error groups which exceeds the threshold' - ); - expect( - tmpl.actions.email.email.body.html.replace(//g, '\n') - ).toMatchSnapshot(); - }); - - it('should format template correctly', () => { - expect(tmpl).toMatchSnapshot(); - }); - - it('should return watch id', async () => { - const id = createWatchSpy.mock.calls[0][0].id; - expect(createWatchResponse).toEqual(id); - }); -}); - -// Recursively iterate a nested structure and render strings as mustache templates -type InputOutput = string | string[] | Record; -function renderMustache( - input: InputOutput, - ctx: Record -): InputOutput { - if (isString(input)) { - return mustache.render(input, { - ctx, - join: () => (text: string, render: any) => render(`{{${text}}}`, { ctx }), - }); - } - - if (isArray(input)) { - return input.map((itemValue) => renderMustache(itemValue, ctx)); - } - - if (isObject(input)) { - return Object.keys(input).reduce((acc, key) => { - const value = (input as any)[key]; - - return { ...acc, [key]: renderMustache(value, ctx) }; - }, {}); - } - - return input; -} diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/esResponse.ts b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/esResponse.ts deleted file mode 100644 index e17cb54b52b5c..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/esResponse.ts +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const esResponse = { - took: 454, - timed_out: false, - _shards: { - total: 10, - successful: 10, - skipped: 0, - failed: 0, - }, - hits: { - total: 23287, - max_score: 0, - hits: [], - }, - aggregations: { - error_groups: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: '63925d00b445cdf4b532dd09d185f5c6', - doc_count: 7761, - sample: { - hits: { - total: 7761, - max_score: null, - hits: [ - { - _index: 'apm-7.0.0-alpha1-error-2018.04.25', - _id: 'qH7C_WIBcmGuKeCHJvvT', - _score: null, - _source: { - '@timestamp': '2018-04-25T17:03:02.296Z', - error: { - log: { - message: 'this is a string', - }, - grouping_key: '63925d00b445cdf4b532dd09d185f5c6', - }, - }, - sort: [1524675782296], - }, - ], - }, - }, - }, - { - key: '89bb1a1f644c7f4bbe8d1781b5cb5fd5', - doc_count: 7752, - sample: { - hits: { - total: 7752, - max_score: null, - hits: [ - { - _index: 'apm-7.0.0-alpha1-error-2018.04.25', - _id: '_3_D_WIBcmGuKeCHFwOW', - _score: null, - _source: { - '@timestamp': '2018-04-25T17:04:03.504Z', - error: { - exception: [ - { - handled: true, - message: 'foo', - }, - ], - culprit: ' (server/coffee.js)', - grouping_key: '89bb1a1f644c7f4bbe8d1781b5cb5fd5', - }, - }, - sort: [1524675843504], - }, - ], - }, - }, - }, - { - key: '7a17ea60604e3531bd8de58645b8631f', - doc_count: 3887, - sample: { - hits: { - total: 3887, - max_score: null, - hits: [ - { - _index: 'apm-7.0.0-alpha1-error-2018.04.25', - _id: 'dn_D_WIBcmGuKeCHQgXJ', - _score: null, - _source: { - '@timestamp': '2018-04-25T17:04:14.575Z', - error: { - exception: [ - { - handled: false, - message: 'socket hang up', - }, - ], - culprit: 'createHangUpError (_http_client.js)', - grouping_key: '7a17ea60604e3531bd8de58645b8631f', - }, - }, - sort: [1524675854575], - }, - ], - }, - }, - }, - { - key: 'b9e1027f29c221763f864f6fa2ad9f5e', - doc_count: 3886, - sample: { - hits: { - total: 3886, - max_score: null, - hits: [ - { - _index: 'apm-7.0.0-alpha1-error-2018.04.25', - _id: 'dX_D_WIBcmGuKeCHQgXJ', - _score: null, - _source: { - '@timestamp': '2018-04-25T17:04:14.533Z', - error: { - exception: [ - { - handled: false, - message: 'this will not get captured by express', - }, - ], - culprit: ' (server/coffee.js)', - grouping_key: 'b9e1027f29c221763f864f6fa2ad9f5e', - }, - }, - sort: [1524675854533], - }, - ], - }, - }, - }, - ], - }, - }, -}; diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts deleted file mode 100644 index 151c4abb9fce3..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { isEmpty } from 'lodash'; -import url from 'url'; -import uuid from 'uuid'; -import { HttpSetup } from 'kibana/public'; -import { - ERROR_CULPRIT, - ERROR_EXC_HANDLED, - ERROR_EXC_MESSAGE, - ERROR_GROUP_ID, - ERROR_LOG_MESSAGE, - PROCESSOR_EVENT, - SERVICE_NAME, -} from '../../../../../common/elasticsearch_fieldnames'; -import { createWatch } from '../../../../services/rest/watcher'; - -function getSlackPathUrl(slackUrl?: string) { - if (slackUrl) { - const { path } = url.parse(slackUrl); - return path; - } -} - -export interface Schedule { - interval?: string; - daily?: { - at: string; - }; -} - -interface Arguments { - http: HttpSetup; - emails: string[]; - schedule: Schedule; - serviceName: string; - slackUrl?: string; - threshold: number; - timeRange: { - value: number; - unit: string; - }; - apmIndexPatternTitle: string; -} - -interface Actions { - log_error: { logging: { text: string } }; - slack_webhook?: Record; - email?: Record; -} - -export async function createErrorGroupWatch({ - http, - emails = [], - schedule, - serviceName, - slackUrl, - threshold, - timeRange, - apmIndexPatternTitle, -}: Arguments) { - const id = `apm-${uuid.v4()}`; - - const slackUrlPath = getSlackPathUrl(slackUrl); - const emailTemplate = i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.emailTemplateText', - { - defaultMessage: - 'Your service {serviceName} has error groups which exceeds {threshold} occurrences within {timeRange}{br}' + - '{br}' + - '{errorGroupsBuckets}{br}' + - '{errorLogMessage}{br}' + - '{errorCulprit}N/A{slashErrorCulprit}{br}' + - '{docCountParam} occurrences{br}' + - '{slashErrorGroupsBucket}', - values: { - serviceName: '"{{ctx.metadata.serviceName}}"', - threshold: '{{ctx.metadata.threshold}}', - timeRange: - '"{{ctx.metadata.timeRangeValue}}{{ctx.metadata.timeRangeUnit}}"', - errorGroupsBuckets: - '{{#ctx.payload.aggregations.error_groups.buckets}}', - errorLogMessage: - '{{sample.hits.hits.0._source.error.log.message}}{{^sample.hits.hits.0._source.error.log.message}}{{sample.hits.hits.0._source.error.exception.0.message}}{{/sample.hits.hits.0._source.error.log.message}}', - errorCulprit: - '{{sample.hits.hits.0._source.error.culprit}}{{^sample.hits.hits.0._source.error.culprit}}', - slashErrorCulprit: '{{/sample.hits.hits.0._source.error.culprit}}', - docCountParam: '{{doc_count}}', - slashErrorGroupsBucket: - '{{/ctx.payload.aggregations.error_groups.buckets}}', - br: '
', - }, - } - ); - - const slackTemplate = i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.slackTemplateText', - { - defaultMessage: `Your service {serviceName} has error groups which exceeds {threshold} occurrences within {timeRange} -{errorGroupsBuckets} -{errorLogMessage} -{errorCulprit}N/A{slashErrorCulprit} -{docCountParam} occurrences -{slashErrorGroupsBucket}`, - values: { - serviceName: '"{{ctx.metadata.serviceName}}"', - threshold: '{{ctx.metadata.threshold}}', - timeRange: - '"{{ctx.metadata.timeRangeValue}}{{ctx.metadata.timeRangeUnit}}"', - errorGroupsBuckets: - '{{#ctx.payload.aggregations.error_groups.buckets}}', - errorLogMessage: - '>*{{sample.hits.hits.0._source.error.log.message}}{{^sample.hits.hits.0._source.error.log.message}}{{sample.hits.hits.0._source.error.exception.0.message}}{{/sample.hits.hits.0._source.error.log.message}}*', - errorCulprit: - '>{{#sample.hits.hits.0._source.error.culprit}}`{{sample.hits.hits.0._source.error.culprit}}`{{/sample.hits.hits.0._source.error.culprit}}{{^sample.hits.hits.0._source.error.culprit}}', - slashErrorCulprit: '{{/sample.hits.hits.0._source.error.culprit}}', - docCountParam: '>{{doc_count}}', - slashErrorGroupsBucket: - '{{/ctx.payload.aggregations.error_groups.buckets}}', - }, - } - ); - - const actions: Actions = { - log_error: { logging: { text: emailTemplate } }, - }; - - const body = { - metadata: { - emails, - trigger: i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.triggerText', - { - defaultMessage: 'This value must be changed in trigger section', - } - ), - serviceName, - threshold, - timeRangeValue: timeRange.value, - timeRangeUnit: timeRange.unit, - slackUrlPath, - }, - trigger: { - schedule, - }, - input: { - search: { - request: { - indices: [apmIndexPatternTitle], - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: '{{ctx.metadata.serviceName}}' } }, - { term: { [PROCESSOR_EVENT]: 'error' } }, - { - range: { - '@timestamp': { - gte: - 'now-{{ctx.metadata.timeRangeValue}}{{ctx.metadata.timeRangeUnit}}', - }, - }, - }, - ], - }, - }, - aggs: { - error_groups: { - terms: { - min_doc_count: '{{ctx.metadata.threshold}}', - field: ERROR_GROUP_ID, - size: 10, - order: { - _count: 'desc', - }, - }, - aggs: { - sample: { - top_hits: { - _source: [ - ERROR_LOG_MESSAGE, - ERROR_EXC_MESSAGE, - ERROR_EXC_HANDLED, - ERROR_CULPRIT, - ERROR_GROUP_ID, - '@timestamp', - ], - sort: [ - { - '@timestamp': 'desc', - }, - ], - size: 1, - }, - }, - }, - }, - }, - }, - }, - }, - }, - condition: { - script: { - source: - 'return ctx.payload.aggregations.error_groups.buckets.length > 0', - }, - }, - actions, - }; - - if (slackUrlPath) { - body.actions.slack_webhook = { - webhook: { - scheme: 'https', - host: 'hooks.slack.com', - port: 443, - method: 'POST', - path: '{{ctx.metadata.slackUrlPath}}', - headers: { - 'Content-Type': 'application/json', - }, - body: `__json__::${JSON.stringify({ - text: slackTemplate, - })}`, - }, - }; - } - - if (!isEmpty(emails)) { - body.actions.email = { - email: { - to: '{{#join}}ctx.metadata.emails{{/join}}', - subject: i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.emailSubjectText', - { - defaultMessage: - '{serviceName} has error groups which exceeds the threshold', - values: { serviceName: '"{{ctx.metadata.serviceName}}"' }, - } - ), - body: { - html: emailTemplate, - }, - }, - }; - } - - await createWatch({ - http, - id, - watch: body, - }); - return id; -} diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx deleted file mode 100644 index 0a7dcbd0be3df..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiButtonEmpty, EuiContextMenu, EuiPopover } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import { WatcherFlyout } from './WatcherFlyout'; -import { ApmPluginContext } from '../../../../context/ApmPluginContext'; - -interface Props { - urlParams: IUrlParams; -} -interface State { - isPopoverOpen: boolean; - activeFlyout: FlyoutName; -} -type FlyoutName = null | 'Watcher'; - -export class ServiceIntegrations extends React.Component { - static contextType = ApmPluginContext; - context!: React.ContextType; - - public state: State = { isPopoverOpen: false, activeFlyout: null }; - - public getWatcherPanelItems = () => { - const { core } = this.context; - - return [ - { - name: i18n.translate( - 'xpack.apm.serviceDetails.integrationsMenu.enableWatcherErrorReportsButtonLabel', - { - defaultMessage: 'Enable watcher error reports', - } - ), - icon: 'watchesApp', - onClick: () => { - this.closePopover(); - this.openFlyout('Watcher'); - }, - }, - { - name: i18n.translate( - 'xpack.apm.serviceDetails.integrationsMenu.viewWatchesButtonLabel', - { - defaultMessage: 'View existing watches', - } - ), - icon: 'watchesApp', - href: core.http.basePath.prepend( - '/app/management/insightsAndAlerting/watcher' - ), - target: '_blank', - onClick: () => this.closePopover(), - }, - ]; - }; - - public openPopover = () => - this.setState({ - isPopoverOpen: true, - }); - - public closePopover = () => - this.setState({ - isPopoverOpen: false, - }); - - public openFlyout = (name: FlyoutName) => - this.setState({ activeFlyout: name }); - - public closeFlyouts = () => this.setState({ activeFlyout: null }); - - public render() { - const button = ( - - {i18n.translate( - 'xpack.apm.serviceDetails.integrationsMenu.integrationsButtonLabel', - { - defaultMessage: 'Integrations', - } - )} - - ); - - return ( - <> - - - - - - ); - } -} diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx index 2d52ad88d20dc..4488a962d0ba8 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx @@ -14,7 +14,6 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { ApmHeader } from '../../shared/ApmHeader'; import { ServiceDetailTabs } from './ServiceDetailTabs'; -import { ServiceIntegrations } from './ServiceIntegrations'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { AlertIntegrations } from './AlertIntegrations'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; @@ -54,9 +53,6 @@ export function ServiceDetails({ tab }: Props) {

{serviceName}

- - - {isAlertingAvailable && ( Date: Tue, 14 Jul 2020 23:48:18 -0700 Subject: [PATCH 78/82] [test] Skips flaky Saved Objects Management test Signed-off-by: Tyler Smalley --- .../apps/saved_objects_management/edit_saved_object.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/apps/saved_objects_management/edit_saved_object.ts b/test/functional/apps/saved_objects_management/edit_saved_object.ts index 0e2ff44ff62ef..aac6178b34e1d 100644 --- a/test/functional/apps/saved_objects_management/edit_saved_object.ts +++ b/test/functional/apps/saved_objects_management/edit_saved_object.ts @@ -67,7 +67,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }; // Flaky: https://github.com/elastic/kibana/issues/68400 - describe('saved objects edition page', () => { + describe.skip('saved objects edition page', () => { beforeEach(async () => { await esArchiver.load('saved_objects_management/edit_saved_object'); }); From 21156d6f189b6e7bd943f98f604e4661d7ae7a25 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Wed, 15 Jul 2020 00:55:48 -0600 Subject: [PATCH 79/82] [SIEM][Detection Engine][Lists] Adds specific endpoint_list REST API and API for abilities to auto-create the endpoint_list if it gets deleted (#71792) * Adds specific endpoint_list REST API and API for abilities to autocreate the endpoint_list if it gets deleted * Added the check against prepackaged list * Updated to use LIST names * Removed the namespace where it does not belong * Updates per code review an extra space that was added Co-authored-by: Elastic Machine --- x-pack/plugins/lists/common/constants.ts | 25 +++ .../create_endpoint_list_item_schema.ts | 63 ++++++++ .../delete_endpoint_list_item_schema.ts | 23 +++ .../request/find_endpoint_list_item_schema.ts | 37 +++++ .../find_exception_list_item_schema.ts | 2 +- .../lists/common/schemas/request/index.ts | 7 +- .../request/read_endpoint_list_item_schema.ts | 31 ++++ .../update_endpoint_list_item_schema.ts | 66 ++++++++ .../routes/create_endpoint_list_item_route.ts | 86 ++++++++++ .../routes/create_endpoint_list_route.ts | 63 ++++++++ .../routes/delete_endpoint_list_item_route.ts | 72 +++++++++ .../routes/find_endpoint_list_item_route.ts | 77 +++++++++ x-pack/plugins/lists/server/routes/index.ts | 7 + .../lists/server/routes/init_routes.ts | 19 ++- .../routes/read_endpoint_list_item_route.ts | 69 ++++++++ .../routes/update_endpoint_list_item_route.ts | 91 +++++++++++ .../update_exception_list_item_route.ts | 15 +- .../scripts/delete_endpoint_list_item.sh | 16 ++ .../delete_endpoint_list_item_by_id.sh | 16 ++ .../new/endpoint_list_item.json | 21 +++ .../updates/simple_update_item.json | 2 +- .../scripts/find_endpoint_list_items.sh | 20 +++ .../server/scripts/get_endpoint_list_item.sh | 15 ++ .../scripts/get_endpoint_list_item_by_id.sh | 18 +++ .../server/scripts/post_endpoint_list.sh | 21 +++ .../server/scripts/post_endpoint_list_item.sh | 30 ++++ .../server/scripts/update_endpoint_item.sh | 30 ++++ .../exception_lists/create_endpoint_list.ts | 65 ++++++++ .../exception_lists/create_exception_list.ts | 2 +- .../exception_lists/exception_list_client.ts | 149 ++++++++++++++++++ .../exception_list_client_types.ts | 43 +++++ .../exception_lists/find_exception_list.ts | 2 +- .../exception_lists/get_exception_list.ts | 3 +- .../exception_lists/update_exception_list.ts | 2 +- .../update_exception_list_item.ts | 1 - .../server/services/exception_lists/utils.ts | 16 +- .../rules/add_prepackaged_rules_route.ts | 4 + .../routes/rules/create_rules_route.ts | 3 +- 38 files changed, 1204 insertions(+), 28 deletions(-) create mode 100644 x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts create mode 100644 x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts create mode 100644 x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts create mode 100644 x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts create mode 100644 x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts create mode 100644 x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts create mode 100644 x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts create mode 100755 x-pack/plugins/lists/server/scripts/delete_endpoint_list_item.sh create mode 100755 x-pack/plugins/lists/server/scripts/delete_endpoint_list_item_by_id.sh create mode 100644 x-pack/plugins/lists/server/scripts/exception_lists/new/endpoint_list_item.json create mode 100755 x-pack/plugins/lists/server/scripts/find_endpoint_list_items.sh create mode 100755 x-pack/plugins/lists/server/scripts/get_endpoint_list_item.sh create mode 100755 x-pack/plugins/lists/server/scripts/get_endpoint_list_item_by_id.sh create mode 100755 x-pack/plugins/lists/server/scripts/post_endpoint_list.sh create mode 100755 x-pack/plugins/lists/server/scripts/post_endpoint_list_item.sh create mode 100755 x-pack/plugins/lists/server/scripts/update_endpoint_item.sh create mode 100644 x-pack/plugins/lists/server/services/exception_lists/create_endpoint_list.ts diff --git a/x-pack/plugins/lists/common/constants.ts b/x-pack/plugins/lists/common/constants.ts index af29b3aa53ded..7bb83cddd4331 100644 --- a/x-pack/plugins/lists/common/constants.ts +++ b/x-pack/plugins/lists/common/constants.ts @@ -23,3 +23,28 @@ export const EXCEPTION_LIST_ITEM_URL = '/api/exception_lists/items'; */ export const EXCEPTION_LIST_NAMESPACE_AGNOSTIC = 'exception-list-agnostic'; export const EXCEPTION_LIST_NAMESPACE = 'exception-list'; + +/** + * Specific routes for the single global space agnostic endpoint list + */ +export const ENDPOINT_LIST_URL = '/api/endpoint_list'; + +/** + * Specific routes for the single global space agnostic endpoint list. These are convenience + * routes where they are going to try and create the global space agnostic endpoint list if it + * does not exist yet or if it was deleted at some point and re-create it before adding items to + * the list + */ +export const ENDPOINT_LIST_ITEM_URL = '/api/endpoint_list/items'; + +/** + * This ID is used for _both_ the Saved Object ID and for the list_id + * for the single global space agnostic endpoint list + */ +export const ENDPOINT_LIST_ID = 'endpoint_list'; + +/** The name of the single global space agnostic endpoint list */ +export const ENDPOINT_LIST_NAME = 'Elastic Endpoint Exception List'; + +/** The description of the single global space agnostic endpoint list */ +export const ENDPOINT_LIST_DESCRIPTION = 'Elastic Endpoint Exception List'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts new file mode 100644 index 0000000000000..5311c7a43cdb5 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { + ItemId, + Tags, + _Tags, + _tags, + description, + exceptionListItemType, + meta, + name, + tags, +} from '../common/schemas'; +import { Identity, RequiredKeepUndefined } from '../../types'; +import { CreateCommentsArray, DefaultCreateCommentsArray, DefaultEntryArray } from '../types'; +import { EntriesArray } from '../types/entries'; +import { DefaultUuid } from '../../siem_common_deps'; + +export const createEndpointListItemSchema = t.intersection([ + t.exact( + t.type({ + description, + name, + type: exceptionListItemType, + }) + ), + t.exact( + t.partial({ + _tags, // defaults to empty array if not set during decode + comments: DefaultCreateCommentsArray, // defaults to empty array if not set during decode + entries: DefaultEntryArray, // defaults to empty array if not set during decode + item_id: DefaultUuid, // defaults to GUID (uuid v4) if not set during decode + meta, // defaults to undefined if not set during decode + tags, // defaults to empty array if not set during decode + }) + ), +]); + +export type CreateEndpointListItemSchemaPartial = Identity< + t.TypeOf +>; +export type CreateEndpointListItemSchema = RequiredKeepUndefined< + t.TypeOf +>; + +// This type is used after a decode since some things are defaults after a decode. +export type CreateEndpointListItemSchemaDecoded = Identity< + Omit & { + _tags: _Tags; + comments: CreateCommentsArray; + tags: Tags; + item_id: ItemId; + entries: EntriesArray; + } +>; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts new file mode 100644 index 0000000000000..311af3a4c0437 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { id, item_id } from '../common/schemas'; + +export const deleteEndpointListItemSchema = t.exact( + t.partial({ + id, + item_id, + }) +); + +export type DeleteEndpointListItemSchema = t.TypeOf; + +// This type is used after a decode since some things are defaults after a decode. +export type DeleteEndpointListItemSchemaDecoded = DeleteEndpointListItemSchema; diff --git a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts new file mode 100644 index 0000000000000..c9ee46994d720 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { filter, sort_field, sort_order } from '../common/schemas'; +import { RequiredKeepUndefined } from '../../types'; +import { StringToPositiveNumber } from '../types/string_to_positive_number'; + +export const findEndpointListItemSchema = t.exact( + t.partial({ + filter, // defaults to undefined if not set during decode + page: StringToPositiveNumber, // defaults to undefined if not set during decode + per_page: StringToPositiveNumber, // defaults to undefined if not set during decode + sort_field, // defaults to undefined if not set during decode + sort_order, // defaults to undefined if not set during decode + }) +); + +export type FindEndpointListItemSchemaPartial = t.OutputOf; + +// This type is used after a decode since some things are defaults after a decode. +export type FindEndpointListItemSchemaPartialDecoded = t.TypeOf; + +// This type is used after a decode since some things are defaults after a decode. +export type FindEndpointListItemSchemaDecoded = RequiredKeepUndefined< + FindEndpointListItemSchemaPartialDecoded +>; + +export type FindEndpointListItemSchema = RequiredKeepUndefined< + t.TypeOf +>; diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts index 826da972fe7a3..aa53fa0fd912c 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts @@ -26,7 +26,7 @@ export const findExceptionListItemSchema = t.intersection([ ), t.exact( t.partial({ - filter: EmptyStringArray, // defaults to undefined if not set during decode + filter: EmptyStringArray, // defaults to an empty array [] if not set during decode namespace_type: DefaultNamespaceArray, // defaults to ['single'] if not set during decode page: StringToPositiveNumber, // defaults to undefined if not set during decode per_page: StringToPositiveNumber, // defaults to undefined if not set during decode diff --git a/x-pack/plugins/lists/common/schemas/request/index.ts b/x-pack/plugins/lists/common/schemas/request/index.ts index 7ab3d943f14da..172d73a5c7377 100644 --- a/x-pack/plugins/lists/common/schemas/request/index.ts +++ b/x-pack/plugins/lists/common/schemas/request/index.ts @@ -4,15 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './create_endpoint_list_item_schema'; export * from './create_exception_list_item_schema'; export * from './create_exception_list_schema'; export * from './create_list_item_schema'; export * from './create_list_schema'; +export * from './delete_endpoint_list_item_schema'; export * from './delete_exception_list_item_schema'; export * from './delete_exception_list_schema'; export * from './delete_list_item_schema'; export * from './delete_list_schema'; export * from './export_list_item_query_schema'; +export * from './find_endpoint_list_item_schema'; export * from './find_exception_list_item_schema'; export * from './find_exception_list_schema'; export * from './find_list_item_schema'; @@ -20,10 +23,12 @@ export * from './find_list_schema'; export * from './import_list_item_schema'; export * from './patch_list_item_schema'; export * from './patch_list_schema'; -export * from './read_exception_list_item_schema'; +export * from './read_endpoint_list_item_schema'; export * from './read_exception_list_schema'; +export * from './read_exception_list_item_schema'; export * from './read_list_item_schema'; export * from './read_list_schema'; +export * from './update_endpoint_list_item_schema'; export * from './update_exception_list_item_schema'; export * from './update_exception_list_schema'; export * from './import_list_item_query_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts new file mode 100644 index 0000000000000..22750f5db6a1d --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { id, item_id } from '../common/schemas'; +import { RequiredKeepUndefined } from '../../types'; + +export const readEndpointListItemSchema = t.exact( + t.partial({ + id, + item_id, + }) +); + +export type ReadEndpointListItemSchemaPartial = t.TypeOf; + +// This type is used after a decode since some things are defaults after a decode. +export type ReadEndpointListItemSchemaPartialDecoded = ReadEndpointListItemSchemaPartial; + +// This type is used after a decode since some things are defaults after a decode. +export type ReadEndpointListItemSchemaDecoded = RequiredKeepUndefined< + ReadEndpointListItemSchemaPartialDecoded +>; + +export type ReadEndpointListItemSchema = RequiredKeepUndefined; diff --git a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts new file mode 100644 index 0000000000000..dbe38f6d468e2 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.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; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { + Tags, + _Tags, + _tags, + description, + exceptionListItemType, + id, + meta, + name, + tags, +} from '../common/schemas'; +import { Identity, RequiredKeepUndefined } from '../../types'; +import { + DefaultEntryArray, + DefaultUpdateCommentsArray, + EntriesArray, + UpdateCommentsArray, +} from '../types'; + +export const updateEndpointListItemSchema = t.intersection([ + t.exact( + t.type({ + description, + name, + type: exceptionListItemType, + }) + ), + t.exact( + t.partial({ + _tags, // defaults to empty array if not set during decode + comments: DefaultUpdateCommentsArray, // defaults to empty array if not set during decode + entries: DefaultEntryArray, // defaults to empty array if not set during decode + id, // defaults to undefined if not set during decode + item_id: t.union([t.string, t.undefined]), + meta, // defaults to undefined if not set during decode + tags, // defaults to empty array if not set during decode + }) + ), +]); + +export type UpdateEndpointListItemSchemaPartial = Identity< + t.TypeOf +>; +export type UpdateEndpointListItemSchema = RequiredKeepUndefined< + t.TypeOf +>; + +// This type is used after a decode since some things are defaults after a decode. +export type UpdateEndpointListItemSchemaDecoded = Identity< + Omit & { + _tags: _Tags; + comments: UpdateCommentsArray; + tags: Tags; + entries: EntriesArray; + } +>; diff --git a/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts new file mode 100644 index 0000000000000..b6eacc3b7dd04 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; +import { + CreateEndpointListItemSchemaDecoded, + createEndpointListItemSchema, + exceptionListItemSchema, +} from '../../common/schemas'; + +import { getExceptionListClient } from './utils/get_exception_list_client'; + +export const createEndpointListItemRoute = (router: IRouter): void => { + router.post( + { + options: { + tags: ['access:lists'], + }, + path: ENDPOINT_LIST_ITEM_URL, + validate: { + body: buildRouteValidation< + typeof createEndpointListItemSchema, + CreateEndpointListItemSchemaDecoded + >(createEndpointListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { + name, + _tags, + tags, + meta, + comments, + description, + entries, + item_id: itemId, + type, + } = request.body; + const exceptionLists = getExceptionListClient(context); + const exceptionListItem = await exceptionLists.getEndpointListItem({ + id: undefined, + itemId, + }); + if (exceptionListItem != null) { + return siemResponse.error({ + body: `exception list item id: "${itemId}" already exists`, + statusCode: 409, + }); + } else { + const createdList = await exceptionLists.createEndpointListItem({ + _tags, + comments, + description, + entries, + itemId, + meta, + name, + tags, + type, + }); + const [validated, errors] = validate(createdList, exceptionListItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts new file mode 100644 index 0000000000000..5d0f3599729b3 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; +import * as t from 'io-ts'; + +import { ENDPOINT_LIST_URL } from '../../common/constants'; +import { buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; +import { exceptionListSchema } from '../../common/schemas'; + +import { getExceptionListClient } from './utils/get_exception_list_client'; + +/** + * This creates the endpoint list if it does not exist. If it does exist, + * this will conflict but continue. This is intended to be as fast as possible so it tries + * each and every time it is called to create the endpoint_list and just ignores any + * conflict so at worse case only one round trip happens per API call. If any error other than conflict + * happens this will return that error. If the list already exists this will return an empty + * object. + * @param router The router to use. + */ +export const createEndpointListRoute = (router: IRouter): void => { + router.post( + { + options: { + tags: ['access:lists'], + }, + path: ENDPOINT_LIST_URL, + validate: false, + }, + async (context, _, response) => { + const siemResponse = buildSiemResponse(response); + try { + // Our goal is be fast as possible and block the least amount of + const exceptionLists = getExceptionListClient(context); + const createdList = await exceptionLists.createEndpointList(); + if (createdList != null) { + const [validated, errors] = validate(createdList, t.union([exceptionListSchema, t.null])); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } else { + // We always return ok on a create endpoint list route but with an empty body as + // an additional fetch of the full list would be slower and the UI has everything hard coded + // within it to get the list if it needs details about it. + return response.ok({ body: {} }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts new file mode 100644 index 0000000000000..b8946c542b27e --- /dev/null +++ b/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; +import { + DeleteEndpointListItemSchemaDecoded, + deleteEndpointListItemSchema, + exceptionListItemSchema, +} from '../../common/schemas'; + +import { getErrorMessageExceptionListItem, getExceptionListClient } from './utils'; + +export const deleteEndpointListItemRoute = (router: IRouter): void => { + router.delete( + { + options: { + tags: ['access:lists'], + }, + path: ENDPOINT_LIST_ITEM_URL, + validate: { + query: buildRouteValidation< + typeof deleteEndpointListItemSchema, + DeleteEndpointListItemSchemaDecoded + >(deleteEndpointListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const exceptionLists = getExceptionListClient(context); + const { item_id: itemId, id } = request.query; + if (itemId == null && id == null) { + return siemResponse.error({ + body: 'Either "item_id" or "id" needs to be defined in the request', + statusCode: 400, + }); + } else { + const deleted = await exceptionLists.deleteEndpointListItem({ + id, + itemId, + }); + if (deleted == null) { + return siemResponse.error({ + body: getErrorMessageExceptionListItem({ id, itemId }), + statusCode: 404, + }); + } else { + const [validated, errors] = validate(deleted, exceptionListItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts new file mode 100644 index 0000000000000..7374ff7dc92ea --- /dev/null +++ b/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { ENDPOINT_LIST_ID, ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; +import { + FindEndpointListItemSchemaDecoded, + findEndpointListItemSchema, + foundExceptionListItemSchema, +} from '../../common/schemas'; + +import { getExceptionListClient } from './utils'; + +export const findEndpointListItemRoute = (router: IRouter): void => { + router.get( + { + options: { + tags: ['access:lists'], + }, + path: `${ENDPOINT_LIST_ITEM_URL}/_find`, + validate: { + query: buildRouteValidation< + typeof findEndpointListItemSchema, + FindEndpointListItemSchemaDecoded + >(findEndpointListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const exceptionLists = getExceptionListClient(context); + const { + filter, + page, + per_page: perPage, + sort_field: sortField, + sort_order: sortOrder, + } = request.query; + + const exceptionListItems = await exceptionLists.findEndpointListItem({ + filter, + page, + perPage, + sortField, + sortOrder, + }); + if (exceptionListItems == null) { + // Although I have this line of code here, this is an incredibly rare thing to have + // happen as the findEndpointListItem tries to auto-create the endpoint list if + // does not exist. + return siemResponse.error({ + body: `list id: "${ENDPOINT_LIST_ID}" does not exist`, + statusCode: 404, + }); + } + const [validated, errors] = validate(exceptionListItems, foundExceptionListItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/index.ts b/x-pack/plugins/lists/server/routes/index.ts index 72117c46213fe..0d99d726d232d 100644 --- a/x-pack/plugins/lists/server/routes/index.ts +++ b/x-pack/plugins/lists/server/routes/index.ts @@ -4,17 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './create_endpoint_list_item_route'; +export * from './create_endpoint_list_route'; export * from './create_exception_list_item_route'; export * from './create_exception_list_route'; export * from './create_list_index_route'; export * from './create_list_item_route'; export * from './create_list_route'; +export * from './delete_endpoint_list_item_route'; export * from './delete_exception_list_route'; export * from './delete_exception_list_item_route'; export * from './delete_list_index_route'; export * from './delete_list_item_route'; export * from './delete_list_route'; export * from './export_list_item_route'; +export * from './find_endpoint_list_item_route'; export * from './find_exception_list_item_route'; export * from './find_exception_list_route'; export * from './find_list_item_route'; @@ -23,11 +27,14 @@ export * from './import_list_item_route'; export * from './init_routes'; export * from './patch_list_item_route'; export * from './patch_list_route'; +export * from './read_endpoint_list_item_route'; export * from './read_exception_list_item_route'; export * from './read_exception_list_route'; export * from './read_list_index_route'; export * from './read_list_item_route'; export * from './read_list_route'; +export * from './read_privileges_route'; +export * from './update_endpoint_list_item_route'; export * from './update_exception_list_item_route'; export * from './update_exception_list_route'; export * from './update_list_item_route'; diff --git a/x-pack/plugins/lists/server/routes/init_routes.ts b/x-pack/plugins/lists/server/routes/init_routes.ts index fef7f19f02df2..7e9e956ebf094 100644 --- a/x-pack/plugins/lists/server/routes/init_routes.ts +++ b/x-pack/plugins/lists/server/routes/init_routes.ts @@ -9,20 +9,22 @@ import { IRouter } from 'kibana/server'; import { SecurityPluginSetup } from '../../../security/server'; import { ConfigType } from '../config'; -import { readPrivilegesRoute } from './read_privileges_route'; - import { + createEndpointListItemRoute, + createEndpointListRoute, createExceptionListItemRoute, createExceptionListRoute, createListIndexRoute, createListItemRoute, createListRoute, + deleteEndpointListItemRoute, deleteExceptionListItemRoute, deleteExceptionListRoute, deleteListIndexRoute, deleteListItemRoute, deleteListRoute, exportListItemRoute, + findEndpointListItemRoute, findExceptionListItemRoute, findExceptionListRoute, findListItemRoute, @@ -30,11 +32,14 @@ import { importListItemRoute, patchListItemRoute, patchListRoute, + readEndpointListItemRoute, readExceptionListItemRoute, readExceptionListRoute, readListIndexRoute, readListItemRoute, readListRoute, + readPrivilegesRoute, + updateEndpointListItemRoute, updateExceptionListItemRoute, updateExceptionListRoute, updateListItemRoute, @@ -83,4 +88,14 @@ export const initRoutes = ( updateExceptionListItemRoute(router); deleteExceptionListItemRoute(router); findExceptionListItemRoute(router); + + // endpoint list + createEndpointListRoute(router); + + // endpoint list items + createEndpointListItemRoute(router); + readEndpointListItemRoute(router); + updateEndpointListItemRoute(router); + deleteEndpointListItemRoute(router); + findEndpointListItemRoute(router); }; diff --git a/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts new file mode 100644 index 0000000000000..5e7ed901bf0cb --- /dev/null +++ b/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; +import { + ReadEndpointListItemSchemaDecoded, + exceptionListItemSchema, + readEndpointListItemSchema, +} from '../../common/schemas'; + +import { getErrorMessageExceptionListItem, getExceptionListClient } from './utils'; + +export const readEndpointListItemRoute = (router: IRouter): void => { + router.get( + { + options: { + tags: ['access:lists'], + }, + path: ENDPOINT_LIST_ITEM_URL, + validate: { + query: buildRouteValidation< + typeof readEndpointListItemSchema, + ReadEndpointListItemSchemaDecoded + >(readEndpointListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { id, item_id: itemId } = request.query; + const exceptionLists = getExceptionListClient(context); + if (id != null || itemId != null) { + const exceptionListItem = await exceptionLists.getEndpointListItem({ + id, + itemId, + }); + if (exceptionListItem == null) { + return siemResponse.error({ + body: getErrorMessageExceptionListItem({ id, itemId }), + statusCode: 404, + }); + } else { + const [validated, errors] = validate(exceptionListItem, exceptionListItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } else { + return siemResponse.error({ body: 'id or item_id required', statusCode: 400 }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts new file mode 100644 index 0000000000000..1ecf4e8a9765d --- /dev/null +++ b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { validate } from '../../common/siem_common_deps'; +import { + UpdateEndpointListItemSchemaDecoded, + exceptionListItemSchema, + updateEndpointListItemSchema, +} from '../../common/schemas'; + +import { getExceptionListClient } from '.'; + +export const updateEndpointListItemRoute = (router: IRouter): void => { + router.put( + { + options: { + tags: ['access:lists'], + }, + path: ENDPOINT_LIST_ITEM_URL, + validate: { + body: buildRouteValidation< + typeof updateEndpointListItemSchema, + UpdateEndpointListItemSchemaDecoded + >(updateEndpointListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { + description, + id, + name, + meta, + type, + _tags, + comments, + entries, + item_id: itemId, + tags, + } = request.body; + const exceptionLists = getExceptionListClient(context); + const exceptionListItem = await exceptionLists.updateEndpointListItem({ + _tags, + comments, + description, + entries, + id, + itemId, + meta, + name, + tags, + type, + }); + if (exceptionListItem == null) { + if (id != null) { + return siemResponse.error({ + body: `list item id: "${id}" not found`, + statusCode: 404, + }); + } else { + return siemResponse.error({ + body: `list item item_id: "${itemId}" not found`, + statusCode: 404, + }); + } + } else { + const [validated, errors] = validate(exceptionListItem, exceptionListItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts index 0ec33b7651982..f6c7bcebedc13 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts @@ -62,10 +62,17 @@ export const updateExceptionListItemRoute = (router: IRouter): void => { type, }); if (exceptionListItem == null) { - return siemResponse.error({ - body: `list item id: "${id}" not found`, - statusCode: 404, - }); + if (id != null) { + return siemResponse.error({ + body: `list item id: "${id}" not found`, + statusCode: 404, + }); + } else { + return siemResponse.error({ + body: `list item item_id: "${itemId}" not found`, + statusCode: 404, + }); + } } else { const [validated, errors] = validate(exceptionListItem, exceptionListItemSchema); if (errors != null) { diff --git a/x-pack/plugins/lists/server/scripts/delete_endpoint_list_item.sh b/x-pack/plugins/lists/server/scripts/delete_endpoint_list_item.sh new file mode 100755 index 0000000000000..b668869bbd82f --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/delete_endpoint_list_item.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./delete_endpoint_list_item.sh ${item_id} +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X DELETE "${KIBANA_URL}${SPACE_URL}/api/endpoint_list/items?item_id=$1" | jq . diff --git a/x-pack/plugins/lists/server/scripts/delete_endpoint_list_item_by_id.sh b/x-pack/plugins/lists/server/scripts/delete_endpoint_list_item_by_id.sh new file mode 100755 index 0000000000000..86dcd0ff1debc --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/delete_endpoint_list_item_by_id.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./delete_endpoint_list_item_by_id.sh ${list_id} +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X DELETE "${KIBANA_URL}${SPACE_URL}/api/endpoint_list/items?id=$1" | jq . diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/new/endpoint_list_item.json b/x-pack/plugins/lists/server/scripts/exception_lists/new/endpoint_list_item.json new file mode 100644 index 0000000000000..8ccbe707f204c --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/exception_lists/new/endpoint_list_item.json @@ -0,0 +1,21 @@ +{ + "item_id": "simple_list_item", + "_tags": ["endpoint", "process", "malware", "os:linux"], + "tags": ["user added string for a tag", "malware"], + "type": "simple", + "description": "This is a sample endpoint type exception", + "name": "Sample Endpoint Exception List", + "entries": [ + { + "field": "actingProcess.file.signer", + "operator": "excluded", + "type": "exists" + }, + { + "field": "host.name", + "operator": "included", + "type": "match_any", + "value": ["some host", "another host"] + } + ] +} diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_item.json b/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_item.json index 08bd95b7d124c..da345fb930c04 100644 --- a/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_item.json +++ b/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_item.json @@ -1,5 +1,5 @@ { - "item_id": "endpoint_list_item", + "item_id": "simple_list_item", "_tags": ["endpoint", "process", "malware", "os:windows"], "tags": ["user added string for a tag", "malware"], "type": "simple", diff --git a/x-pack/plugins/lists/server/scripts/find_endpoint_list_items.sh b/x-pack/plugins/lists/server/scripts/find_endpoint_list_items.sh new file mode 100755 index 0000000000000..9372389a70b01 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/find_endpoint_list_items.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Optionally, post at least one list item +# ./post_endpoint_list_item.sh ./exception_lists/new/endpoint_list_item.json +# +# Then you can query it as in: +# Example: ./find_endpoint_list_item.sh +# +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/endpoint_list/items/_find" | jq . diff --git a/x-pack/plugins/lists/server/scripts/get_endpoint_list_item.sh b/x-pack/plugins/lists/server/scripts/get_endpoint_list_item.sh new file mode 100755 index 0000000000000..4f5842048293a --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/get_endpoint_list_item.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./get_endpoint_list_item.sh ${item_id} +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/endpoint_list/items?item_id=$1" | jq . diff --git a/x-pack/plugins/lists/server/scripts/get_endpoint_list_item_by_id.sh b/x-pack/plugins/lists/server/scripts/get_endpoint_list_item_by_id.sh new file mode 100755 index 0000000000000..6e035010014a1 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/get_endpoint_list_item_by_id.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +set -e +./check_env_variables.sh + +# Example: ./get_endpoint_list_item.sh ${id} +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/endpoint_list/items?id=$1" | jq . diff --git a/x-pack/plugins/lists/server/scripts/post_endpoint_list.sh b/x-pack/plugins/lists/server/scripts/post_endpoint_list.sh new file mode 100755 index 0000000000000..e0b179f443547 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/post_endpoint_list.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +LISTS=(${@:-./exception_lists/new/exception_list.json}) + +# Example: ./post_endpoint_list.sh +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/endpoint_list \ + | jq .; diff --git a/x-pack/plugins/lists/server/scripts/post_endpoint_list_item.sh b/x-pack/plugins/lists/server/scripts/post_endpoint_list_item.sh new file mode 100755 index 0000000000000..8235a2ec06eb7 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/post_endpoint_list_item.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +LISTS=(${@:-./exception_lists/new/endpoint_list_item.json}) + +# Example: ./post_endpoint_list_item.sh +# Example: ./post_endpoint_list_item.sh ./exception_lists/new/endpoint_list_item.json +for LIST in "${LISTS[@]}" +do { + [ -e "$LIST" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/endpoint_list/items \ + -d @${LIST} \ + | jq .; +} & +done + +wait diff --git a/x-pack/plugins/lists/server/scripts/update_endpoint_item.sh b/x-pack/plugins/lists/server/scripts/update_endpoint_item.sh new file mode 100755 index 0000000000000..4a6ca3881a323 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/update_endpoint_item.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +LISTS=(${@:-./exception_lists/updates/simple_update_item.json}) + +# Example: ./update_endpoint_list_item.sh +# Example: ./update_endpoint_list_item.sh ./exception_lists/updates/simple_update_item.json +for LIST in "${LISTS[@]}" +do { + [ -e "$LIST" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X PUT ${KIBANA_URL}${SPACE_URL}/api/endpoint_list/items \ + -d @${LIST} \ + | jq .; +} & +done + +wait diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_list.ts new file mode 100644 index 0000000000000..b9a0194e20074 --- /dev/null +++ b/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_list.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClientContract } from 'kibana/server'; +import uuid from 'uuid'; + +import { + ENDPOINT_LIST_DESCRIPTION, + ENDPOINT_LIST_ID, + ENDPOINT_LIST_NAME, +} from '../../../common/constants'; +import { ExceptionListSchema, ExceptionListSoSchema } from '../../../common/schemas'; + +import { getSavedObjectType, transformSavedObjectToExceptionList } from './utils'; + +interface CreateEndpointListOptions { + savedObjectsClient: SavedObjectsClientContract; + user: string; + tieBreaker?: string; +} + +export const createEndpointList = async ({ + savedObjectsClient, + user, + tieBreaker, +}: CreateEndpointListOptions): Promise => { + const savedObjectType = getSavedObjectType({ namespaceType: 'agnostic' }); + const dateNow = new Date().toISOString(); + try { + const savedObject = await savedObjectsClient.create( + savedObjectType, + { + _tags: [], + comments: undefined, + created_at: dateNow, + created_by: user, + description: ENDPOINT_LIST_DESCRIPTION, + entries: undefined, + item_id: undefined, + list_id: ENDPOINT_LIST_ID, + list_type: 'list', + meta: undefined, + name: ENDPOINT_LIST_NAME, + tags: [], + tie_breaker_id: tieBreaker ?? uuid.v4(), + type: 'endpoint', + updated_by: user, + }, + { + // We intentionally hard coding the id so that there can only be one exception list within the space + id: ENDPOINT_LIST_ID, + } + ); + return transformSavedObjectToExceptionList({ savedObject }); + } catch (err) { + if (err.status === 409) { + return null; + } else { + throw err; + } + } +}; diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts index f6a3bca10028d..4da74c7df48bf 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts @@ -68,5 +68,5 @@ export const createExceptionList = async ({ type, updated_by: user, }); - return transformSavedObjectToExceptionList({ namespaceType, savedObject }); + return transformSavedObjectToExceptionList({ savedObject }); }; diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts index 62afda52bd79d..5c9607e2d956d 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts @@ -6,6 +6,7 @@ import { SavedObjectsClientContract } from 'kibana/server'; +import { ENDPOINT_LIST_ID } from '../../../common/constants'; import { ExceptionListItemSchema, ExceptionListSchema, @@ -15,15 +16,20 @@ import { import { ConstructorOptions, + CreateEndpointListItemOptions, CreateExceptionListItemOptions, CreateExceptionListOptions, + DeleteEndpointListItemOptions, DeleteExceptionListItemOptions, DeleteExceptionListOptions, + FindEndpointListItemOptions, FindExceptionListItemOptions, FindExceptionListOptions, FindExceptionListsItemOptions, + GetEndpointListItemOptions, GetExceptionListItemOptions, GetExceptionListOptions, + UpdateEndpointListItemOptions, UpdateExceptionListItemOptions, UpdateExceptionListOptions, } from './exception_list_client_types'; @@ -38,6 +44,7 @@ import { deleteExceptionListItem } from './delete_exception_list_item'; import { findExceptionListItem } from './find_exception_list_item'; import { findExceptionList } from './find_exception_list'; import { findExceptionListsItem } from './find_exception_list_items'; +import { createEndpointList } from './create_endpoint_list'; export class ExceptionListClient { private readonly user: string; @@ -67,6 +74,103 @@ export class ExceptionListClient { return getExceptionListItem({ id, itemId, namespaceType, savedObjectsClient }); }; + /** + * This creates an agnostic space endpoint list if it does not exist. This tries to be + * as fast as possible by ignoring conflict errors and not returning the contents of the + * list if it already exists. + * @returns ExceptionListSchema if it created the endpoint list, otherwise null if it already exists + */ + public createEndpointList = async (): Promise => { + const { savedObjectsClient, user } = this; + return createEndpointList({ + savedObjectsClient, + user, + }); + }; + + /** + * This is the same as "createListItem" except it applies specifically to the agnostic endpoint list and will + * auto-call the "createEndpointList" for you so that you have the best chance of the agnostic endpoint + * being there and existing before the item is inserted into the agnostic endpoint list. + */ + public createEndpointListItem = async ({ + _tags, + comments, + description, + entries, + itemId, + meta, + name, + tags, + type, + }: CreateEndpointListItemOptions): Promise => { + const { savedObjectsClient, user } = this; + await this.createEndpointList(); + return createExceptionListItem({ + _tags, + comments, + description, + entries, + itemId, + listId: ENDPOINT_LIST_ID, + meta, + name, + namespaceType: 'agnostic', + savedObjectsClient, + tags, + type, + user, + }); + }; + + /** + * This is the same as "updateListItem" except it applies specifically to the endpoint list and will + * auto-call the "createEndpointList" for you so that you have the best chance of the endpoint + * being there if it did not exist before. If the list did not exist before, then creating it here will still cause a + * return of null but at least the list exists again. + */ + public updateEndpointListItem = async ({ + _tags, + comments, + description, + entries, + id, + itemId, + meta, + name, + tags, + type, + }: UpdateEndpointListItemOptions): Promise => { + const { savedObjectsClient, user } = this; + await this.createEndpointList(); + return updateExceptionListItem({ + _tags, + comments, + description, + entries, + id, + itemId, + meta, + name, + namespaceType: 'agnostic', + savedObjectsClient, + tags, + type, + user, + }); + }; + + /** + * This is the same as "getExceptionListItem" except it applies specifically to the endpoint list. + */ + public getEndpointListItem = async ({ + itemId, + id, + }: GetEndpointListItemOptions): Promise => { + const { savedObjectsClient } = this; + return getExceptionListItem({ id, itemId, namespaceType: 'agnostic', savedObjectsClient }); + }; + public createExceptionList = async ({ _tags, description, @@ -209,6 +313,22 @@ export class ExceptionListClient { }); }; + /** + * This is the same as "deleteExceptionListItem" except it applies specifically to the endpoint list. + */ + public deleteEndpointListItem = async ({ + id, + itemId, + }: DeleteEndpointListItemOptions): Promise => { + const { savedObjectsClient } = this; + return deleteExceptionListItem({ + id, + itemId, + namespaceType: 'agnostic', + savedObjectsClient, + }); + }; + public findExceptionListItem = async ({ listId, filter, @@ -272,4 +392,33 @@ export class ExceptionListClient { sortOrder, }); }; + + /** + * This is the same as "findExceptionList" except it applies specifically to the endpoint list and will + * auto-call the "createEndpointList" for you so that you have the best chance of the endpoint + * being there if it did not exist before. If the list did not exist before, then creating it here should give you + * a good guarantee that you will get an empty record set rather than null. I keep the null as the return value in + * the off chance that you still might somehow not get into a race condition where the endpoint list does + * not exist because someone deleted it in-between the initial create and then the find. + */ + public findEndpointListItem = async ({ + filter, + perPage, + page, + sortField, + sortOrder, + }: FindEndpointListItemOptions): Promise => { + const { savedObjectsClient } = this; + await this.createEndpointList(); + return findExceptionListItem({ + filter, + listId: ENDPOINT_LIST_ID, + namespaceType: 'agnostic', + page, + perPage, + savedObjectsClient, + sortField, + sortOrder, + }); + }; } diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts index b3070f2d4a70d..89f8310281648 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts @@ -86,12 +86,22 @@ export interface DeleteExceptionListItemOptions { namespaceType: NamespaceType; } +export interface DeleteEndpointListItemOptions { + id: IdOrUndefined; + itemId: ItemIdOrUndefined; +} + export interface GetExceptionListItemOptions { itemId: ItemIdOrUndefined; id: IdOrUndefined; namespaceType: NamespaceType; } +export interface GetEndpointListItemOptions { + itemId: ItemIdOrUndefined; + id: IdOrUndefined; +} + export interface CreateExceptionListItemOptions { _tags: _Tags; comments: CreateCommentsArray; @@ -106,6 +116,18 @@ export interface CreateExceptionListItemOptions { type: ExceptionListItemType; } +export interface CreateEndpointListItemOptions { + _tags: _Tags; + comments: CreateCommentsArray; + entries: EntriesArray; + itemId: ItemId; + name: Name; + description: Description; + meta: MetaOrUndefined; + tags: Tags; + type: ExceptionListItemType; +} + export interface UpdateExceptionListItemOptions { _tags: _TagsOrUndefined; comments: UpdateCommentsArray; @@ -120,6 +142,19 @@ export interface UpdateExceptionListItemOptions { type: ExceptionListItemTypeOrUndefined; } +export interface UpdateEndpointListItemOptions { + _tags: _TagsOrUndefined; + comments: UpdateCommentsArray; + entries: EntriesArrayOrUndefined; + id: IdOrUndefined; + itemId: ItemIdOrUndefined; + name: NameOrUndefined; + description: DescriptionOrUndefined; + meta: MetaOrUndefined; + tags: TagsOrUndefined; + type: ExceptionListItemTypeOrUndefined; +} + export interface FindExceptionListItemOptions { listId: ListId; namespaceType: NamespaceType; @@ -130,6 +165,14 @@ export interface FindExceptionListItemOptions { sortOrder: SortOrderOrUndefined; } +export interface FindEndpointListItemOptions { + filter: FilterOrUndefined; + perPage: PerPageOrUndefined; + page: PageOrUndefined; + sortField: SortFieldOrUndefined; + sortOrder: SortOrderOrUndefined; +} + export interface FindExceptionListsItemOptions { listId: NonEmptyStringArrayDecoded; namespaceType: NamespaceTypeArray; diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts index 899ed30863770..84cc7ba2f1021 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts @@ -48,7 +48,7 @@ export const findExceptionList = async ({ sortOrder, type: savedObjectType, }); - return transformSavedObjectsToFoundExceptionList({ namespaceType, savedObjectsFindResponse }); + return transformSavedObjectsToFoundExceptionList({ savedObjectsFindResponse }); }; export const getExceptionListFilter = ({ diff --git a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts index 8f511d140b0ff..a5c1e2e5c6bc9 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts @@ -35,7 +35,7 @@ export const getExceptionList = async ({ if (id != null) { try { const savedObject = await savedObjectsClient.get(savedObjectType, id); - return transformSavedObjectToExceptionList({ namespaceType, savedObject }); + return transformSavedObjectToExceptionList({ savedObject }); } catch (err) { if (SavedObjectsErrorHelpers.isNotFoundError(err)) { return null; @@ -55,7 +55,6 @@ export const getExceptionList = async ({ }); if (savedObject.saved_objects[0] != null) { return transformSavedObjectToExceptionList({ - namespaceType, savedObject: savedObject.saved_objects[0], }); } else { diff --git a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts index e4d6718ddc29f..a739366c67331 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts @@ -69,6 +69,6 @@ export const updateExceptionList = async ({ updated_by: user, } ); - return transformSavedObjectUpdateToExceptionList({ exceptionList, namespaceType, savedObject }); + return transformSavedObjectUpdateToExceptionList({ exceptionList, savedObject }); } }; diff --git a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts index 2059c730d809f..a5ed1e38df374 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts @@ -93,7 +93,6 @@ export const updateExceptionListItem = async ({ ); return transformSavedObjectUpdateToExceptionListItem({ exceptionListItem, - namespaceType, savedObject, }); } diff --git a/x-pack/plugins/lists/server/services/exception_lists/utils.ts b/x-pack/plugins/lists/server/services/exception_lists/utils.ts index 3ef2c337e80b6..ded39933fe9d8 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/utils.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/utils.ts @@ -67,10 +67,8 @@ export const getSavedObjectTypes = ({ export const transformSavedObjectToExceptionList = ({ savedObject, - namespaceType, }: { savedObject: SavedObject; - namespaceType: NamespaceType; }): ExceptionListSchema => { const dateNow = new Date().toISOString(); const { @@ -102,7 +100,7 @@ export const transformSavedObjectToExceptionList = ({ list_id, meta, name, - namespace_type: namespaceType, + namespace_type: getExceptionListType({ savedObjectType: savedObject.type }), tags, tie_breaker_id, type: exceptionListType.is(type) ? type : 'detection', @@ -114,11 +112,9 @@ export const transformSavedObjectToExceptionList = ({ export const transformSavedObjectUpdateToExceptionList = ({ exceptionList, savedObject, - namespaceType, }: { exceptionList: ExceptionListSchema; savedObject: SavedObjectsUpdateResponse; - namespaceType: NamespaceType; }): ExceptionListSchema => { const dateNow = new Date().toISOString(); const { @@ -138,7 +134,7 @@ export const transformSavedObjectUpdateToExceptionList = ({ list_id: exceptionList.list_id, meta: meta ?? exceptionList.meta, name: name ?? exceptionList.name, - namespace_type: namespaceType, + namespace_type: getExceptionListType({ savedObjectType: savedObject.type }), tags: tags ?? exceptionList.tags, tie_breaker_id: exceptionList.tie_breaker_id, type: exceptionListType.is(type) ? type : exceptionList.type, @@ -200,11 +196,9 @@ export const transformSavedObjectToExceptionListItem = ({ export const transformSavedObjectUpdateToExceptionListItem = ({ exceptionListItem, savedObject, - namespaceType, }: { exceptionListItem: ExceptionListItemSchema; savedObject: SavedObjectsUpdateResponse; - namespaceType: NamespaceType; }): ExceptionListItemSchema => { const dateNow = new Date().toISOString(); const { @@ -239,7 +233,7 @@ export const transformSavedObjectUpdateToExceptionListItem = ({ list_id: exceptionListItem.list_id, meta: meta ?? exceptionListItem.meta, name: name ?? exceptionListItem.name, - namespace_type: namespaceType, + namespace_type: getExceptionListType({ savedObjectType: savedObject.type }), tags: tags ?? exceptionListItem.tags, tie_breaker_id: exceptionListItem.tie_breaker_id, type: exceptionListItemType.is(type) ? type : exceptionListItem.type, @@ -265,14 +259,12 @@ export const transformSavedObjectsToFoundExceptionListItem = ({ export const transformSavedObjectsToFoundExceptionList = ({ savedObjectsFindResponse, - namespaceType, }: { savedObjectsFindResponse: SavedObjectsFindResponse; - namespaceType: NamespaceType; }): FoundExceptionListSchema => { return { data: savedObjectsFindResponse.saved_objects.map((savedObject) => - transformSavedObjectToExceptionList({ namespaceType, savedObject }) + transformSavedObjectToExceptionList({ savedObject }) ), page: savedObjectsFindResponse.page, per_page: savedObjectsFindResponse.per_page, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index 1226be71f63f5..b1f6f73b09627 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -55,6 +55,10 @@ export const addPrepackedRulesRoute = ( if (!siemClient || !alertsClient) { return siemResponse.error({ statusCode: 404 }); } + + // This will create the endpoint list if it does not exist yet + await context.lists?.getExceptionListClient().createEndpointList(); + const rulesFromFileSystem = getPrepackagedRules(); const prepackagedRules = await getExistingPrepackagedRules({ alertsClient }); const rulesToInstall = getRulesToInstall(rulesFromFileSystem, prepackagedRules); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts index edad3dd8a4f21..482edb9925557 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -97,7 +97,6 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void // TODO: Fix these either with an is conversion or by better typing them within io-ts const actions: RuleAlertAction[] = actionsRest as RuleAlertAction[]; const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[]; - const alertsClient = context.alerting?.getAlertsClient(); const clusterClient = context.core.elasticsearch.legacy.client; const savedObjectsClient = context.core.savedObjects.client; @@ -127,6 +126,8 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void }); } } + // This will create the endpoint list if it does not exist yet + await context.lists?.getExceptionListClient().createEndpointList(); const createdRule = await createRules({ alertsClient, From 667b72f9e8777d0138fb13e5488d3a1fb1271a05 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Wed, 15 Jul 2020 10:35:24 +0300 Subject: [PATCH 80/82] use fixed isChromeVisible method (#71813) --- x-pack/test/functional_embedded/tests/iframe_embedded.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/x-pack/test/functional_embedded/tests/iframe_embedded.ts b/x-pack/test/functional_embedded/tests/iframe_embedded.ts index f05d70b6cb3e8..e3468efe3d1da 100644 --- a/x-pack/test/functional_embedded/tests/iframe_embedded.ts +++ b/x-pack/test/functional_embedded/tests/iframe_embedded.ts @@ -13,9 +13,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const config = getService('config'); const testSubjects = getService('testSubjects'); + const retry = getService('retry'); - // Flaky: https://github.com/elastic/kibana/issues/70928 - describe.skip('in iframe', () => { + describe('in iframe', () => { it('should open Kibana for logged-in user', async () => { const isChromeHiddenBefore = await PageObjects.common.isChromeHidden(); expect(isChromeHiddenBefore).to.be(true); @@ -36,8 +36,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const iframe = await testSubjects.find('iframe_embedded'); await browser.switchToFrame(iframe); - const isChromeHidden = await PageObjects.common.isChromeHidden(); - expect(isChromeHidden).to.be(false); + await retry.waitFor('page rendered for a logged-in user', async () => { + return await PageObjects.common.isChromeVisible(); + }); }); }); } From 75582eb4ae59d85fff95661ef8dfabfbb7197d28 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Wed, 15 Jul 2020 03:51:31 -0400 Subject: [PATCH 81/82] [SECURITY] Timeline bug 7.9 (#71748) * remove delay of rendering row * Fix flyout timeline to behave as we wanted * Fix tabs on timeline page * disable sensor visibility when you have less than 100 events in timeline * Fix container to fit content and not take all the place that it wants * do not update timeline time when switching top nav * fix timeline url in case * review I Co-authored-by: Elastic Machine --- .../cases/components/add_comment/index.tsx | 40 ++-------- .../cases/components/all_cases/index.test.tsx | 8 -- .../cases/components/all_cases/index.tsx | 25 +++++-- .../components/all_cases_modal/index.tsx | 2 +- .../public/cases/components/create/index.tsx | 6 +- .../user_action_markdown.test.tsx | 2 + .../user_action_tree/user_action_markdown.tsx | 30 +------- .../components/utils/use_timeline_click.tsx | 40 ++++++++++ .../events_viewer/events_viewer.tsx | 3 +- .../common/components/markdown/index.test.tsx | 14 +++- .../common/components/markdown/index.tsx | 10 ++- .../components/markdown_editor/form.tsx | 2 +- .../components/markdown_editor/index.tsx | 26 ++++--- .../components/url_state/use_url_state.tsx | 34 +++++++-- .../components/with_hover_actions/index.tsx | 8 +- .../components/alerts_table/index.tsx | 9 ++- .../components/flyout/pane/index.tsx | 1 + .../components/graph_overlay/index.tsx | 73 ++++++++++--------- .../components/manage_timeline/index.tsx | 12 +++ .../open_timeline/use_timeline_types.tsx | 21 +++--- .../components/timeline/body/events/index.tsx | 5 +- .../timeline/body/events/stateful_event.tsx | 44 ++--------- .../components/timeline/body/index.test.tsx | 5 +- .../components/timeline/body/index.tsx | 10 ++- .../timeline/body/stateful_body.tsx | 7 +- .../timelines/components/timeline/index.tsx | 2 + .../components/timeline/properties/index.tsx | 6 +- 27 files changed, 245 insertions(+), 200 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/cases/components/utils/use_timeline_click.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx index a830b299d655b..980083e8e9d20 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx @@ -8,7 +8,6 @@ import { EuiButton, EuiLoadingSpinner } from '@elastic/eui'; import React, { useCallback, useEffect } from 'react'; import styled from 'styled-components'; -import { useDispatch } from 'react-redux'; import { CommentRequest } from '../../../../../case/common/api'; import { usePostComment } from '../../containers/use_post_comment'; import { Case } from '../../containers/types'; @@ -19,12 +18,7 @@ import { Form, useForm, UseField } from '../../../shared_imports'; import * as i18n from './translations'; import { schema } from './schema'; -import { - dispatchUpdateTimeline, - queryTimelineById, -} from '../../../timelines/components/open_timeline/helpers'; -import { updateIsLoading as dispatchUpdateIsLoading } from '../../../timelines/store/timeline/actions'; -import { useApolloClient } from '../../../common/utils/apollo_context'; +import { useTimelineClick } from '../utils/use_timeline_click'; const MySpinner = styled(EuiLoadingSpinner)` position: absolute; @@ -53,8 +47,7 @@ export const AddComment = React.memo( options: { stripEmptyFields: false }, schema, }); - const dispatch = useDispatch(); - const apolloClient = useApolloClient(); + const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( form, 'comment' @@ -68,30 +61,9 @@ export const AddComment = React.memo( `${comment}${comment.length > 0 ? '\n\n' : ''}${insertQuote}` ); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [insertQuote]); + }, [form, insertQuote]); - const handleTimelineClick = useCallback( - (timelineId: string) => { - queryTimelineById({ - apolloClient, - timelineId, - updateIsLoading: ({ - id: currentTimelineId, - isLoading: isLoadingTimeline, - }: { - id: string; - isLoading: boolean; - }) => - dispatch( - dispatchUpdateIsLoading({ id: currentTimelineId, isLoading: isLoadingTimeline }) - ), - updateTimeline: dispatchUpdateTimeline(dispatch), - }); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [apolloClient] - ); + const handleTimelineClick = useTimelineClick(); const onSubmit = useCallback(async () => { const { isValid, data } = await form.submit(); @@ -102,8 +74,8 @@ export const AddComment = React.memo( postComment(data, onCommentPosted); form.reset(); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [form, onCommentPosted, onCommentSaving]); + }, [form, onCommentPosted, onCommentSaving, postComment]); + return ( {isLoading && showLoading && } diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx index ed8ec432f7df5..d8acda8ec4f33 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx @@ -29,14 +29,6 @@ const useGetCasesMock = useGetCases as jest.Mock; const useGetCasesStatusMock = useGetCasesStatus as jest.Mock; const useUpdateCasesMock = useUpdateCases as jest.Mock; -jest.mock('react-router-dom', () => { - const originalModule = jest.requireActual('react-router-dom'); - return { - ...originalModule, - useHistory: jest.fn(), - }; -}); - jest.mock('../../../common/components/link_to'); describe('AllCases', () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx index bf134a02dd822..f46dd9e858c7f 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx @@ -5,7 +5,6 @@ */ /* eslint-disable react-hooks/exhaustive-deps */ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useHistory } from 'react-router-dom'; import { EuiBasicTable, EuiContextMenuPanel, @@ -50,6 +49,8 @@ import { ConfigureCaseButton } from '../configure_cases/button'; import { ERROR_PUSH_SERVICE_CALLOUT_TITLE } from '../use_push_to_service/translations'; import { LinkButton } from '../../../common/components/links'; import { SecurityPageName } from '../../../app/types'; +import { useKibana } from '../../../common/lib/kibana'; +import { APP_ID } from '../../../../common/constants'; const Div = styled.div` margin-top: ${({ theme }) => theme.eui.paddingSizes.m}; @@ -81,13 +82,13 @@ const getSortField = (field: string): SortFieldCase => { }; interface AllCasesProps { - onRowClick?: (id: string) => void; + onRowClick?: (id?: string) => void; isModal?: boolean; userCanCrud: boolean; } export const AllCases = React.memo( - ({ onRowClick = () => {}, isModal = false, userCanCrud }) => { - const history = useHistory(); + ({ onRowClick, isModal = false, userCanCrud }) => { + const { navigateToApp } = useKibana().services.application; const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.case); const { actionLicense } = useGetActionLicense(); const { @@ -234,9 +235,15 @@ export const AllCases = React.memo( const goToCreateCase = useCallback( (ev) => { ev.preventDefault(); - history.push(getCreateCaseUrl(urlSearch)); + if (isModal && onRowClick != null) { + onRowClick(); + } else { + navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { + path: getCreateCaseUrl(urlSearch), + }); + } }, - [history, urlSearch] + [navigateToApp, isModal, onRowClick, urlSearch] ); const actions = useMemo( @@ -445,7 +452,11 @@ export const AllCases = React.memo( rowProps={(item) => isModal ? { - onClick: () => onRowClick(item.id), + onClick: () => { + if (onRowClick != null) { + onRowClick(item.id); + } + }, } : {} } diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.tsx index d2ca0f0cd02ee..d8f2e5293ee1b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.tsx @@ -19,7 +19,7 @@ import * as i18n from './translations'; interface AllCasesModalProps { onCloseCaseModal: () => void; showCaseModal: boolean; - onRowClick: (id: string) => void; + onRowClick: (id?: string) => void; } export const AllCasesModalComponent = ({ diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx index 9f078c725c3cf..1a2697bb132b0 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx @@ -33,6 +33,7 @@ import * as i18n from '../../translations'; import { MarkdownEditorForm } from '../../../common/components//markdown_editor/form'; import { useGetTags } from '../../containers/use_get_tags'; import { getCaseDetailsUrl } from '../../../common/components/link_to'; +import { useTimelineClick } from '../utils/use_timeline_click'; export const CommonUseField = getUseField({ component: Field }); @@ -87,6 +88,7 @@ export const Create = React.memo(() => { form, 'description' ); + const handleTimelineClick = useTimelineClick(); const onSubmit = useCallback(async () => { const { isValid, data } = await form.submit(); @@ -94,8 +96,7 @@ export const Create = React.memo(() => { // `postCase`'s type is incorrect, it actually returns a promise await postCase(data); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [form]); + }, [form, postCase]); const handleSetIsCancel = useCallback(() => { history.push('/'); @@ -145,6 +146,7 @@ export const Create = React.memo(() => { dataTestSubj: 'caseDescription', idAria: 'caseDescription', isDisabled: isLoading, + onClickTimeline: handleTimelineClick, onCursorPositionUpdate: handleCursorChange, topRightContent: ( { expect(queryTimelineByIdSpy).toBeCalledWith({ apolloClient: mockUseApolloClient(), + graphEventId: '', timelineId, updateIsLoading: expect.any(Function), updateTimeline: expect.any(Function), @@ -62,6 +63,7 @@ describe('UserActionMarkdown ', () => { wrapper.find(`[data-test-subj="markdown-timeline-link"]`).first().simulate('click'); expect(queryTimelineByIdSpy).toBeCalledWith({ apolloClient: mockUseApolloClient(), + graphEventId: '', timelineId, updateIsLoading: expect.any(Function), updateTimeline: expect.any(Function), diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx index b3a5f1e0158d8..0a8167049266f 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx @@ -8,7 +8,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/e import React, { useCallback } from 'react'; import styled, { css } from 'styled-components'; -import { useDispatch } from 'react-redux'; import * as i18n from '../case_view/translations'; import { Markdown } from '../../../common/components/markdown'; import { Form, useForm, UseField } from '../../../shared_imports'; @@ -16,13 +15,7 @@ import { schema, Content } from './schema'; import { InsertTimelinePopover } from '../../../timelines/components/timeline/insert_timeline_popover'; import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'; import { MarkdownEditorForm } from '../../../common/components//markdown_editor/form'; -import { - dispatchUpdateTimeline, - queryTimelineById, -} from '../../../timelines/components/open_timeline/helpers'; - -import { updateIsLoading as dispatchUpdateIsLoading } from '../../../timelines/store/timeline/actions'; -import { useApolloClient } from '../../../common/utils/apollo_context'; +import { useTimelineClick } from '../utils/use_timeline_click'; const ContentWrapper = styled.div` ${({ theme }) => css` @@ -44,8 +37,6 @@ export const UserActionMarkdown = ({ onChangeEditable, onSaveContent, }: UserActionMarkdownProps) => { - const dispatch = useDispatch(); - const apolloClient = useApolloClient(); const { form } = useForm({ defaultValue: { content }, options: { stripEmptyFields: false }, @@ -59,24 +50,7 @@ export const UserActionMarkdown = ({ onChangeEditable(id); }, [id, onChangeEditable]); - const handleTimelineClick = useCallback( - (timelineId: string) => { - queryTimelineById({ - apolloClient, - timelineId, - updateIsLoading: ({ - id: currentTimelineId, - isLoading, - }: { - id: string; - isLoading: boolean; - }) => dispatch(dispatchUpdateIsLoading({ id: currentTimelineId, isLoading })), - updateTimeline: dispatchUpdateTimeline(dispatch), - }); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [apolloClient] - ); + const handleTimelineClick = useTimelineClick(); const handleSaveAction = useCallback(async () => { const { isValid, data } = await form.submit(); diff --git a/x-pack/plugins/security_solution/public/cases/components/utils/use_timeline_click.tsx b/x-pack/plugins/security_solution/public/cases/components/utils/use_timeline_click.tsx new file mode 100644 index 0000000000000..971bc87c8cdd2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/utils/use_timeline_click.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { useApolloClient } from '../../../common/utils/apollo_context'; +import { + dispatchUpdateTimeline, + queryTimelineById, +} from '../../../timelines/components/open_timeline/helpers'; +import { updateIsLoading as dispatchUpdateIsLoading } from '../../../timelines/store/timeline/actions'; + +export const useTimelineClick = () => { + const dispatch = useDispatch(); + const apolloClient = useApolloClient(); + + const handleTimelineClick = useCallback( + (timelineId: string, graphEventId?: string) => { + queryTimelineById({ + apolloClient, + graphEventId, + timelineId, + updateIsLoading: ({ + id: currentTimelineId, + isLoading, + }: { + id: string; + isLoading: boolean; + }) => dispatch(dispatchUpdateIsLoading({ id: currentTimelineId, isLoading })), + updateTimeline: dispatchUpdateTimeline(dispatch), + }); + }, + [apolloClient, dispatch] + ); + + return handleTimelineClick; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index 5e0d5a6e9b099..6e6ba4911be26 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -106,8 +106,7 @@ const EventsViewerComponent: React.FC = ({ useEffect(() => { setIsTimelineLoading({ id, isLoading: isQueryLoading }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isQueryLoading]); + }, [id, isQueryLoading, setIsTimelineLoading]); const { queryFields, title, unit } = useMemo(() => getManageTimelineById(id), [ getManageTimelineById, diff --git a/x-pack/plugins/security_solution/public/common/components/markdown/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/markdown/index.test.tsx index 69620eb1f4341..e30391982ee7a 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown/index.test.tsx @@ -157,7 +157,19 @@ describe('Markdown', () => { ); wrapper.find('[data-test-subj="markdown-timeline-link"]').first().simulate('click'); - expect(onClickTimeline).toHaveBeenCalledWith(timelineId); + expect(onClickTimeline).toHaveBeenCalledWith(timelineId, ''); + }); + + test('timeline link onClick calls onClickTimeline with timelineId and graphEventId', () => { + const graphEventId = '2bc51864784c'; + const markdownWithTimelineAndGraphEventLink = `A link to a timeline [timeline](http://localhost:5601/app/siem#/timelines?timeline=(id:'${timelineId}',isOpen:!t,graphEventId:'${graphEventId}'))`; + + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="markdown-timeline-link"]').first().simulate('click'); + + expect(onClickTimeline).toHaveBeenCalledWith(timelineId, graphEventId); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown/index.tsx index 1a4c9cb71a77e..1d73c3cb8a2aa 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown/index.tsx @@ -7,6 +7,7 @@ /* eslint-disable react/display-name */ import { EuiLink, EuiTableRow, EuiTableRowCell, EuiText, EuiToolTip } from '@elastic/eui'; +import { clone } from 'lodash/fp'; import React from 'react'; import ReactMarkdown from 'react-markdown'; import styled, { css } from 'styled-components'; @@ -38,7 +39,7 @@ const REL_NOREFERRER = 'noreferrer'; export const Markdown = React.memo<{ disableLinks?: boolean; raw?: string; - onClickTimeline?: (timelineId: string) => void; + onClickTimeline?: (timelineId: string, graphEventId?: string) => void; size?: 'xs' | 's' | 'm'; }>(({ disableLinks = false, onClickTimeline, raw, size = 's' }) => { const markdownRenderers = { @@ -63,11 +64,14 @@ export const Markdown = React.memo<{ ), link: ({ children, href }: { children: React.ReactNode[]; href?: string }) => { if (onClickTimeline != null && href != null && href.indexOf(`timelines?timeline=(id:`) > -1) { - const timelineId = href.split('timelines?timeline=(id:')[1].split("'")[1] ?? ''; + const timelineId = clone(href).split('timeline=(id:')[1].split("'")[1] ?? ''; + const graphEventId = href.includes('graphEventId:') + ? clone(href).split('graphEventId:')[1].split("'")[1] ?? '' + : ''; return ( onClickTimeline(timelineId)} + onClick={() => onClickTimeline(timelineId, graphEventId)} data-test-subj="markdown-timeline-link" > {children} diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/form.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/form.tsx index f9efbc5705b92..2cc3fe05a2215 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/form.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/form.tsx @@ -16,7 +16,7 @@ interface IMarkdownEditorForm { field: FieldHook; idAria: string; isDisabled: boolean; - onClickTimeline?: (timelineId: string) => void; + onClickTimeline?: (timelineId: string, graphEventId?: string) => void; onCursorPositionUpdate?: (cursorPosition: CursorPosition) => void; placeholder?: string; topRightContent?: React.ReactNode; diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.tsx index d92952992d997..c40b3910ec152 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.tsx @@ -74,7 +74,7 @@ export const MarkdownEditor = React.memo<{ content: string; isDisabled?: boolean; onChange: (description: string) => void; - onClickTimeline?: (timelineId: string) => void; + onClickTimeline?: (timelineId: string, graphEventId?: string) => void; onCursorPositionUpdate?: (cursorPosition: CursorPosition) => void; placeholder?: string; }>( @@ -95,15 +95,18 @@ export const MarkdownEditor = React.memo<{ [onChange] ); - const setCursorPosition = (e: React.ChangeEvent) => { - if (onCursorPositionUpdate) { - onCursorPositionUpdate({ - start: e!.target!.selectionStart ?? 0, - end: e!.target!.selectionEnd ?? 0, - }); - } - return false; - }; + const setCursorPosition = useCallback( + (e: React.ChangeEvent) => { + if (onCursorPositionUpdate) { + onCursorPositionUpdate({ + start: e!.target!.selectionStart ?? 0, + end: e!.target!.selectionEnd ?? 0, + }); + } + return false; + }, + [onCursorPositionUpdate] + ); const tabs = useMemo( () => [ @@ -135,8 +138,7 @@ export const MarkdownEditor = React.memo<{ ), }, ], - // eslint-disable-next-line react-hooks/exhaustive-deps - [content, isDisabled, placeholder] + [content, handleOnChange, isDisabled, onClickTimeline, placeholder, setCursorPosition] ); return ( diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx index c97be1fdfb99b..644fd46cb6aae 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx @@ -18,6 +18,7 @@ import { getTitle, replaceStateInLocation, updateUrlStateString, + decodeRisonUrlState, } from './helpers'; import { UrlStateContainerPropTypes, @@ -26,8 +27,10 @@ import { KeyUrlState, ALL_URL_STATE_KEYS, UrlStateToRedux, + UrlState, } from './types'; import { SecurityPageName } from '../../../app/types'; +import { TimelineUrl } from '../../../timelines/store/timeline/model'; function usePrevious(value: PreviousLocationUrlState) { const ref = useRef(value); @@ -37,6 +40,21 @@ function usePrevious(value: PreviousLocationUrlState) { return ref.current; } +const updateTimelineAtinitialization = ( + urlKey: CONSTANTS, + newUrlStateString: string, + urlState: UrlState +) => { + let updateUrlState = true; + if (urlKey === CONSTANTS.timeline) { + const timeline = decodeRisonUrlState(newUrlStateString); + if (timeline != null && urlState.timeline.id === timeline.id) { + updateUrlState = false; + } + } + return updateUrlState; +}; + export const useUrlStateHooks = ({ detailName, indexPattern, @@ -78,13 +96,15 @@ export const useUrlStateHooks = ({ getParamFromQueryString(getQueryStringFromLocation(mySearch), urlKey) ?? newUrlStateString; if (isInitializing || !deepEqual(updatedUrlStateString, newUrlStateString)) { - urlStateToUpdate = [ - ...urlStateToUpdate, - { - urlKey, - newUrlStateString: updatedUrlStateString, - }, - ]; + if (updateTimelineAtinitialization(urlKey, newUrlStateString, urlState)) { + urlStateToUpdate = [ + ...urlStateToUpdate, + { + urlKey, + newUrlStateString: updatedUrlStateString, + }, + ]; + } } } } else if ( diff --git a/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx b/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx index 361779a4a33b2..97705533689e9 100644 --- a/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx @@ -17,6 +17,10 @@ const WithHoverActionsPopover = (styled(EuiPopover as any)` } ` as unknown) as typeof EuiPopover; +const Container = styled.div` + width: fit-content; +`; + interface Props { /** * Always show the hover menu contents (default: false) @@ -75,7 +79,7 @@ export const WithHoverActions = React.memo( }, [closePopOverTrigger]); return ( -
+ ( > {isOpen ? <>{hoverContent} : null} -
+
); } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 87c631b80e38b..405ba0719a910 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -374,7 +374,7 @@ export const AlertsTableComponent: React.FC = ({ } }, [defaultFilters, filterGroup]); const { filterManager } = useKibana().services.data.query; - const { initializeTimeline, setTimelineRowActions } = useManageTimeline(); + const { initializeTimeline, setTimelineRowActions, setIndexToAdd } = useManageTimeline(); useEffect(() => { initializeTimeline({ @@ -383,6 +383,7 @@ export const AlertsTableComponent: React.FC = ({ filterManager, footerText: i18n.TOTAL_COUNT_OF_ALERTS, id: timelineId, + indexToAdd: defaultIndices, loadingText: i18n.LOADING_ALERTS, selectAll: canUserCRUD ? selectAll : false, timelineRowActions: () => [getInvestigateInResolverAction({ dispatch, timelineId })], @@ -390,6 +391,7 @@ export const AlertsTableComponent: React.FC = ({ }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useEffect(() => { setTimelineRowActions({ id: timelineId, @@ -398,6 +400,11 @@ export const AlertsTableComponent: React.FC = ({ }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [additionalActions]); + + useEffect(() => { + setIndexToAdd({ id: timelineId, indexToAdd: defaultIndices }); + }, [timelineId, defaultIndices, setIndexToAdd]); + const headerFilterGroup = useMemo( () => , [onFilterGroupChangedCallback] diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx index 8c03d82aafafb..1616738897b0a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx @@ -31,6 +31,7 @@ const EuiFlyoutContainer = styled.div` z-index: 4001; min-width: 150px; width: auto; + animation: none; } `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx index 0b5b51d6f1fb2..085f0863c7b27 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx @@ -12,7 +12,7 @@ import styled from 'styled-components'; import { SecurityPageName } from '../../../app/types'; import { AllCasesModal } from '../../../cases/components/all_cases_modal'; -import { getCaseDetailsUrl } from '../../../common/components/link_to'; +import { getCaseDetailsUrl, getCreateCaseUrl } from '../../../common/components/link_to'; import { APP_ID } from '../../../../common/constants'; import { useKibana } from '../../../common/lib/kibana'; import { State } from '../../../common/store'; @@ -28,6 +28,7 @@ import { import { Resolver } from '../../../resolver/view'; import * as i18n from './translations'; +import { TimelineType } from '../../../../common/types/timeline'; const OverlayContainer = styled.div<{ bodyHeight?: number }>` height: ${({ bodyHeight }) => (bodyHeight ? `${bodyHeight}px` : 'auto')}; @@ -44,6 +45,7 @@ interface OwnProps { bodyHeight?: number; graphEventId?: string; timelineId: string; + timelineType: TimelineType; } const GraphOverlayComponent = ({ @@ -52,6 +54,7 @@ const GraphOverlayComponent = ({ status, timelineId, title, + timelineType, }: OwnProps & PropsFromRedux) => { const dispatch = useDispatch(); const { navigateToApp } = useKibana().services.application; @@ -65,20 +68,20 @@ const GraphOverlayComponent = ({ timelineSelectors.selectTimeline(state, timelineId) ); const onRowClick = useCallback( - (id: string) => { + (id?: string) => { onCloseCaseModal(); - dispatch( - setInsertTimeline({ - graphEventId, - timelineId, - timelineSavedObjectId: currentTimeline.savedObjectId, - timelineTitle: title.length > 0 ? title : UNTITLED_TIMELINE, - }) - ); - navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { - path: getCaseDetailsUrl({ id }), + path: id != null ? getCaseDetailsUrl({ id }) : getCreateCaseUrl(), + }).then(() => { + dispatch( + setInsertTimeline({ + graphEventId, + timelineId, + timelineSavedObjectId: currentTimeline.savedObjectId, + timelineTitle: title.length > 0 ? title : UNTITLED_TIMELINE, + }) + ); }); }, [currentTimeline, dispatch, graphEventId, navigateToApp, onCloseCaseModal, timelineId, title] @@ -93,28 +96,30 @@ const GraphOverlayComponent = ({ {i18n.BACK_TO_EVENTS}
- - - - - - - - - - + {timelineType === TimelineType.default && ( + + + + + + + + + + + )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx index 7882185cbd9d6..dba8506add0ad 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx @@ -138,6 +138,7 @@ const reducerManageTimeline = ( }; interface UseTimelineManager { + getIndexToAddById: (id: string) => string[] | null; getManageTimelineById: (id: string) => ManageTimeline; getTimelineFilterManager: (id: string) => FilterManager | undefined; initializeTimeline: (newTimeline: ManageTimelineInit) => void; @@ -216,9 +217,19 @@ const useTimelineManager = (manageTimelineForTesting?: ManageTimelineById): UseT }, [initializeTimeline, state] ); + const getIndexToAddById = useCallback( + (id: string): string[] | null => { + if (state[id] != null) { + return state[id].indexToAdd; + } + return getTimelineDefaults(id).indexToAdd; + }, + [state] + ); const isManagedTimeline = useCallback((id: string): boolean => state[id] != null, [state]); return { + getIndexToAddById, getManageTimelineById, getTimelineFilterManager, initializeTimeline, @@ -231,6 +242,7 @@ const useTimelineManager = (manageTimelineForTesting?: ManageTimelineById): UseT const init = { getManageTimelineById: (id: string) => getTimelineDefaults(id), + getIndexToAddById: (id: string) => null, getTimelineFilterManager: () => undefined, setIndexToAdd: () => undefined, isManagedTimeline: () => false, diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx index bee94db348872..7d54bb2209850 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx @@ -90,14 +90,17 @@ export const useTimelineTypes = ({ ); const onFilterClicked = useCallback( - (tabId) => { - if (tabId === timelineType) { - setTimelineTypes(null); - } else { - setTimelineTypes(tabId); - } + (tabId, tabStyle: TimelineTabsStyle) => { + setTimelineTypes((prevTimelineTypes) => { + if (tabId === prevTimelineTypes && tabStyle === TimelineTabsStyle.filter) { + return null; + } else if (prevTimelineTypes !== tabId) { + setTimelineTypes(tabId); + } + return prevTimelineTypes; + }); }, - [timelineType, setTimelineTypes] + [setTimelineTypes] ); const timelineTabs = useMemo(() => { @@ -112,7 +115,7 @@ export const useTimelineTypes = ({ href={tab.href} onClick={(ev) => { tab.onClick(ev); - onFilterClicked(tab.id); + onFilterClicked(tab.id, TimelineTabsStyle.tab); }} > {tab.name} @@ -133,7 +136,7 @@ export const useTimelineTypes = ({ numFilters={tab.count} onClick={(ev: { preventDefault: () => void }) => { tab.onClick(ev); - onFilterClicked(tab.id); + onFilterClicked(tab.id, TimelineTabsStyle.filter); }} withNext={tab.withNext} > diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx index 9f0c4747db057..ca7a64db58c95 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx @@ -9,7 +9,6 @@ import React from 'react'; import { BrowserFields, DocValueFields } from '../../../../../common/containers/source'; import { TimelineItem, TimelineNonEcsData } from '../../../../../graphql/types'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; -import { maxDelay } from '../../../../../common/lib/helpers/scheduler'; import { Note } from '../../../../../common/lib/note'; import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers'; import { @@ -81,12 +80,13 @@ const EventsComponent: React.FC = ({ {data.map((event, i) => ( = ({ isEventViewer={isEventViewer} key={`${event._id}_${event._index}`} loadingEventIds={loadingEventIds} - maxDelay={maxDelay(i)} onColumnResized={onColumnResized} onPinEvent={onPinEvent} onRowSelected={onRowSelected} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index f93a152211a66..344fbb59bbe57 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useRef, useState, useCallback } from 'react'; +import React, { useRef, useState, useCallback } from 'react'; import { useSelector } from 'react-redux'; import uuid from 'uuid'; import VisibilitySensor from 'react-visibility-sensor'; @@ -12,7 +12,6 @@ import VisibilitySensor from 'react-visibility-sensor'; import { BrowserFields, DocValueFields } from '../../../../../common/containers/source'; import { TimelineDetailsQuery } from '../../../../containers/details'; import { TimelineItem, DetailItem, TimelineNonEcsData } from '../../../../../graphql/types'; -import { requestIdleCallbackViaScheduler } from '../../../../../common/lib/helpers/scheduler'; import { Note } from '../../../../../common/lib/note'; import { ColumnHeaderOptions, TimelineModel } from '../../../../../timelines/store/timeline/model'; import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers'; @@ -43,13 +42,13 @@ interface Props { browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; columnRenderers: ColumnRenderer[]; + disableSensorVisibility: boolean; docValueFields: DocValueFields[]; event: TimelineItem; eventIdToNoteIds: Readonly>; getNotesByIds: (noteIds: string[]) => Note[]; isEventViewer?: boolean; loadingEventIds: Readonly; - maxDelay?: number; onColumnResized: OnColumnResized; onPinEvent: OnPinEvent; onRowSelected: OnRowSelected; @@ -109,6 +108,7 @@ const StatefulEventComponent: React.FC = ({ containerElementRef, columnHeaders, columnRenderers, + disableSensorVisibility = true, docValueFields, event, eventIdToNoteIds, @@ -116,7 +116,6 @@ const StatefulEventComponent: React.FC = ({ isEventViewer = false, isEventPinned = false, loadingEventIds, - maxDelay = 0, onColumnResized, onPinEvent, onRowSelected, @@ -130,7 +129,6 @@ const StatefulEventComponent: React.FC = ({ updateNote, }) => { const [expanded, setExpanded] = useState<{ [eventId: string]: boolean }>({}); - const [initialRender, setInitialRender] = useState(false); const [showNotes, setShowNotes] = useState<{ [eventId: string]: boolean }>({}); const timeline = useSelector((state) => { return state.timeline.timelineById['timeline-1']; @@ -160,39 +158,9 @@ const StatefulEventComponent: React.FC = ({ [addNoteToEvent, event, isEventPinned, onPinEvent] ); - /** - * Incrementally loads the events when it mounts by trying to - * see if it resides within a window frame and if it is it will - * indicate to React that it should render its self by setting - * its initialRender to true. - */ - useEffect(() => { - let _isMounted = true; - - requestIdleCallbackViaScheduler( - () => { - if (!initialRender && _isMounted) { - setInitialRender(true); - } - }, - { timeout: maxDelay } - ); - return () => { - _isMounted = false; - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - // Number of current columns plus one for actions. const columnCount = columnHeaders.length + 1; - // If we are not ready to render yet, just return null - // see useEffect() for when it schedules the first - // time this stateful component should be rendered. - if (!initialRender) { - return ; - } - return ( = ({ offset={{ top: TOP_OFFSET, bottom: BOTTOM_OFFSET }} > {({ isVisible }) => { - if (isVisible) { + if (isVisible || disableSensorVisibility) { return ( = ({ } else { // Height place holder for visibility detection as well as re-rendering sections. const height = - divElement.current != null && divElement.current.clientHeight - ? `${divElement.current.clientHeight}px` + divElement.current != null && divElement.current!.clientHeight + ? `${divElement.current!.clientHeight}px` : DEFAULT_ROW_HEIGHT; return ; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index 68a8d474ff5ad..2df6a39f1a3df 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { ReactWrapper } from '@elastic/eui/node_modules/@types/enzyme'; import React from 'react'; import { useSelector } from 'react-redux'; @@ -18,7 +18,7 @@ import { Sort } from './sort'; import { wait } from '../../../../common/lib/helpers'; import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { SELECTOR_TIMELINE_BODY_CLASS_NAME, TimelineBody } from '../styles'; -import { ReactWrapper } from '@elastic/eui/node_modules/@types/enzyme'; +import { TimelineType } from '../../../../../common/types/timeline'; const testBodyHeight = 700; const mockGetNotesByIds = (eventId: string[]) => []; @@ -83,6 +83,7 @@ describe('Body', () => { show: true, sort: mockSort, showCheckboxes: false, + timelineType: TimelineType.default, toggleColumn: jest.fn(), updateNote: jest.fn(), }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index 86bb49fac7f3e..83e44b77802b7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -33,6 +33,7 @@ import { useManageTimeline } from '../../manage_timeline'; import { GraphOverlay } from '../../graph_overlay'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers'; import { TimelineRowAction } from './actions'; +import { TimelineType } from '../../../../../common/types/timeline'; export interface BodyProps { addNoteToEvent: AddNoteToEvent; @@ -64,6 +65,7 @@ export interface BodyProps { show: boolean; showCheckboxes: boolean; sort: Sort; + timelineType: TimelineType; toggleColumn: (column: ColumnHeaderOptions) => void; updateNote: UpdateNote; } @@ -101,6 +103,7 @@ export const Body = React.memo( showCheckboxes, sort, toggleColumn, + timelineType, updateNote, }) => { const containerElementRef = useRef(null); @@ -148,7 +151,12 @@ export const Body = React.memo( return ( <> {graphEventId && ( - + )} ( showCheckboxes, graphEventId, sort, + timelineType, toggleColumn, unPinEvent, updateColumns, @@ -218,6 +219,7 @@ const StatefulBodyComponent = React.memo( show={id === TimelineId.active ? show : true} showCheckboxes={showCheckboxes} sort={sort} + timelineType={timelineType} toggleColumn={toggleColumn} updateNote={onUpdateNote} /> @@ -241,7 +243,8 @@ const StatefulBodyComponent = React.memo( prevProps.show === nextProps.show && prevProps.selectedEventIds === nextProps.selectedEventIds && prevProps.showCheckboxes === nextProps.showCheckboxes && - prevProps.sort === nextProps.sort + prevProps.sort === nextProps.sort && + prevProps.timelineType === nextProps.timelineType ); StatefulBodyComponent.displayName = 'StatefulBodyComponent'; @@ -268,6 +271,7 @@ const makeMapStateToProps = () => { selectedEventIds, show, showCheckboxes, + timelineType, } = timeline; return { @@ -284,6 +288,7 @@ const makeMapStateToProps = () => { selectedEventIds, show, showCheckboxes, + timelineType, }; }; return mapStateToProps; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index 2d7527d8a922c..c170c93ee6083 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -215,6 +215,7 @@ const StatefulTimelineComponent = React.memo( /> ); }, + // eslint-disable-next-line complexity (prevProps, nextProps) => { return ( prevProps.eventType === nextProps.eventType && @@ -223,6 +224,7 @@ const StatefulTimelineComponent = React.memo( prevProps.id === nextProps.id && prevProps.isLive === nextProps.isLive && prevProps.isSaving === nextProps.isSaving && + prevProps.isTimelineExists === nextProps.isTimelineExists && prevProps.itemsPerPage === nextProps.itemsPerPage && prevProps.kqlMode === nextProps.kqlMode && prevProps.kqlQueryExpression === nextProps.kqlQueryExpression && diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx index 6de40725f461c..96a773507a30a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx @@ -25,7 +25,7 @@ import { timelineSelectors } from '../../../store/timeline'; import { setInsertTimeline } from '../../../store/timeline/actions'; import { useKibana } from '../../../../common/lib/kibana'; import { APP_ID } from '../../../../../common/constants'; -import { getCaseDetailsUrl } from '../../../../common/components/link_to'; +import { getCaseDetailsUrl, getCreateCaseUrl } from '../../../../common/components/link_to'; type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void; type UpdateTitle = ({ id, title }: { id: string; title: string }) => void; @@ -111,11 +111,11 @@ export const Properties = React.memo( ); const onRowClick = useCallback( - (id: string) => { + (id?: string) => { onCloseCaseModal(); navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { - path: getCaseDetailsUrl({ id }), + path: id != null ? getCaseDetailsUrl({ id }) : getCreateCaseUrl(), }).then(() => dispatch( setInsertTimeline({ From 4e6f0c60e2785547e0304d66dffcc957b4dc2ec3 Mon Sep 17 00:00:00 2001 From: Bohdan Tsymbala Date: Wed, 15 Jul 2020 10:16:27 +0200 Subject: [PATCH 82/82] Fixed the spacing of child accordion items for policy response dialog. (#71677) --- .../view/details/policy_response.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response.tsx index 8db95f586782c..4cdfaad69eb72 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response.tsx @@ -26,30 +26,36 @@ import { * actions the endpoint took to apply the policy configuration. */ const PolicyResponseConfigAccordion = styled(EuiAccordion)` - > .euiAccordion__triggerWrapper { + .euiAccordion__triggerWrapper { padding: ${(props) => props.theme.eui.paddingSizes.s}; } + &.euiAccordion-isOpen { background-color: ${(props) => props.theme.eui.euiFocusBackgroundColor}; } + .euiAccordion__childWrapper { background-color: ${(props) => props.theme.eui.euiColorLightestShade}; } + .policyResponseAttentionBadge { background-color: ${(props) => props.theme.eui.euiColorDanger}; color: ${(props) => props.theme.eui.euiColorEmptyShade}; } + .euiAccordion__button { :hover, :focus { text-decoration: none; } } + :hover:not(.euiAccordion-isOpen) { background-color: ${(props) => props.theme.eui.euiColorLightestShade}; } .policyResponseActionsAccordion { + .euiAccordion__iconWrapper, svg { height: ${(props) => props.theme.eui.euiIconSizes.small}; width: ${(props) => props.theme.eui.euiIconSizes.small}; @@ -59,6 +65,10 @@ const PolicyResponseConfigAccordion = styled(EuiAccordion)` .policyResponseStatusHealth { width: 100px; } + + .policyResponseMessage { + padding-left: ${(props) => props.theme.eui.paddingSizes.l}; + } `; const ResponseActions = memo( @@ -105,7 +115,7 @@ const ResponseActions = memo( } > -

{statuses.message}

+

{statuses.message}

);