diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md index 0f14215ff1309..07ede291e33d2 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md @@ -17,5 +17,6 @@ export declare type EmbeddableInput = { disabledActions?: string[]; disableTriggers?: boolean; searchSessionId?: string; + syncColors?: boolean; }; ``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.openaddpanelflyout.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.openaddpanelflyout.md index ce97f79b4beb9..add4646375359 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.openaddpanelflyout.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.openaddpanelflyout.md @@ -14,7 +14,7 @@ export declare function openAddPanelFlyout(options: { overlays: OverlayStart; notifications: NotificationsStart; SavedObjectFinder: React.ComponentType; -}): Promise; +}): OverlayRef; ``` ## Parameters @@ -25,5 +25,5 @@ export declare function openAddPanelFlyout(options: { Returns: -`Promise` +`OverlayRef` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md index 1565202e84674..9dfad91c33679 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md @@ -9,7 +9,7 @@ Constructs a new instance of the `ExpressionRenderHandler` class Signature: ```typescript -constructor(element: HTMLElement, { onRenderError, renderMode, hasCompatibleActions, }?: ExpressionRenderHandlerParams); +constructor(element: HTMLElement, { onRenderError, renderMode, syncColors, hasCompatibleActions, }?: ExpressionRenderHandlerParams); ``` ## Parameters @@ -17,5 +17,5 @@ constructor(element: HTMLElement, { onRenderError, renderMode, hasCompatibleActi | Parameter | Type | Description | | --- | --- | --- | | element | HTMLElement | | -| { onRenderError, renderMode, hasCompatibleActions, } | ExpressionRenderHandlerParams | | +| { onRenderError, renderMode, syncColors, hasCompatibleActions, } | ExpressionRenderHandlerParams | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler.md index d65c06bdaed83..1a7050f3ffd4e 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler.md @@ -14,7 +14,7 @@ export declare class ExpressionRenderHandler | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(element, { onRenderError, renderMode, hasCompatibleActions, })](./kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md) | | Constructs a new instance of the ExpressionRenderHandler class | +| [(constructor)(element, { onRenderError, renderMode, syncColors, hasCompatibleActions, })](./kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md) | | Constructs a new instance of the ExpressionRenderHandler class | ## Properties diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md index 22a73fff039e6..4ef1225ae0d7e 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md @@ -25,6 +25,7 @@ export interface IExpressionLoaderParams | [renderMode](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.rendermode.md) | RenderMode | | | [searchContext](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md) | SerializableState | | | [searchSessionId](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchsessionid.md) | string | | +| [syncColors](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.synccolors.md) | boolean | | | [uiState](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.uistate.md) | unknown | | | [variables](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.variables.md) | Record<string, any> | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.synccolors.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.synccolors.md new file mode 100644 index 0000000000000..619f54ad88ef2 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.synccolors.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [IExpressionLoaderParams](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md) > [syncColors](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.synccolors.md) + +## IExpressionLoaderParams.syncColors property + +Signature: + +```typescript +syncColors?: boolean; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.issynccolorsenabled.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.issynccolorsenabled.md new file mode 100644 index 0000000000000..6cdc796bf464b --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.issynccolorsenabled.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [IInterpreterRenderHandlers](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md) > [isSyncColorsEnabled](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.issynccolorsenabled.md) + +## IInterpreterRenderHandlers.isSyncColorsEnabled property + +Signature: + +```typescript +isSyncColorsEnabled: () => boolean; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md index c22c8bc6b6245..0b39a9b4b3ea2 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md @@ -18,6 +18,7 @@ export interface IInterpreterRenderHandlers | [event](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.event.md) | (event: any) => void | | | [getRenderMode](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.getrendermode.md) | () => RenderMode | | | [hasCompatibleActions](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.hascompatibleactions.md) | (event: any) => Promise<boolean> | | +| [isSyncColorsEnabled](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.issynccolorsenabled.md) | () => boolean | | | [onDestroy](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.ondestroy.md) | (fn: () => void) => void | | | [reload](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.reload.md) | () => void | | | [uiState](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.uistate.md) | unknown | This uiState interface is actually PersistedState from the visualizations plugin, but expressions cannot know about vis or it creates a mess of circular dependencies. Downstream consumers of the uiState handler will need to cast for now. | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.issynccolorsenabled.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.issynccolorsenabled.md new file mode 100644 index 0000000000000..71a7e020e65a5 --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.issynccolorsenabled.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [IInterpreterRenderHandlers](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md) > [isSyncColorsEnabled](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.issynccolorsenabled.md) + +## IInterpreterRenderHandlers.isSyncColorsEnabled property + +Signature: + +```typescript +isSyncColorsEnabled: () => boolean; +``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md index 547608f40e6aa..831c9023c7e48 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md @@ -18,6 +18,7 @@ export interface IInterpreterRenderHandlers | [event](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.event.md) | (event: any) => void | | | [getRenderMode](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.getrendermode.md) | () => RenderMode | | | [hasCompatibleActions](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.hascompatibleactions.md) | (event: any) => Promise<boolean> | | +| [isSyncColorsEnabled](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.issynccolorsenabled.md) | () => boolean | | | [onDestroy](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.ondestroy.md) | (fn: () => void) => void | | | [reload](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.reload.md) | () => void | | | [uiState](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.uistate.md) | unknown | This uiState interface is actually PersistedState from the visualizations plugin, but expressions cannot know about vis or it creates a mess of circular dependencies. Downstream consumers of the uiState handler will need to cast for now. | diff --git a/docs/discover/search.asciidoc b/docs/discover/search.asciidoc index 75c6fddb484ac..45f0df5bd773f 100644 --- a/docs/discover/search.asciidoc +++ b/docs/discover/search.asciidoc @@ -74,7 +74,7 @@ status codes, you could enter `status:[400 TO 499]`. codes and have an extension of `php` or `html`, you could enter `status:[400 TO 499] AND (extension:php OR extension:html)`. -IMPORTANT: When you use the Lucene Query Syntax in the *KQL* search bar, {kib} is unable to search on nested objects and perform aggregations across fields that contain nested objects. +IMPORTANT: When you use the Lucene Query Syntax in the *KQL* search bar, {kib} is unable to search on nested objects and perform aggregations across fields that contain nested objects. Using `include_in_parent` or `copy_to` as a workaround can cause {kib} to fail. For more detailed information about the Lucene query syntax, see the @@ -107,7 +107,8 @@ To save the current search: . Click *Save* in the Kibana toolbar. . Enter a name for the search and click *Save*. -To import, export, and delete saved searches, open the main menu, then click *Stack Management > Saved Ojbects*. +To import, export, and delete saved searches, open the main menu, +then click *Stack Management > Saved Objects*. ==== Open a saved search To load a saved search into Discover: diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 99fadb240335a..7e7c8953fd527 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -262,6 +262,10 @@ Hides the "Time" column in *Discover* and in all saved searches on dashboards. Highlights results in *Discover* and saved searches on dashboards. Highlighting slows requests when working on big documents. +[[doctable-legacy]]`doc_table:legacy`:: +Control the way the Discover's table looks and works. Set this property to `true` to revert to the legacy implementation. + + [float] [[kibana-ml-settings]] ==== Machine learning diff --git a/docs/user/dashboard/edit-dashboards.asciidoc b/docs/user/dashboard/edit-dashboards.asciidoc index 7b712b355b315..d7f7dc2d65c85 100644 --- a/docs/user/dashboard/edit-dashboards.asciidoc +++ b/docs/user/dashboard/edit-dashboards.asciidoc @@ -81,6 +81,21 @@ Put the dashboard in *Edit* mode, then use the following options: * To delete, open the panel menu, then select *Delete from dashboard*. When you delete a panel from the dashboard, the visualization or saved search from the panel is still available in Kibana. +[float] +[[sync-colors]] +=== Synchronize colors + +By default, dashboard panels that share a non-gradient based color palette will synchronize their color assignment to improve readability. +Color assignment is based on the series name, and the total number of colors is based on the number of unique series names. + +The color synchronizing logic can make the dashboard less readable when there are too many unique series names. It is possible to disable the synchronization behavior: + +. Put the dashboard in *Edit* mode. + +. Click the "Options" button in the top navigation bar. + +. Disable "Sync color palettes across panels". + [float] [[clone-panels]] === Clone panels diff --git a/package.json b/package.json index 985998e54cb88..61b13a06bffe9 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "@elastic/datemath": "link:packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary", "@elastic/ems-client": "7.11.0", - "@elastic/eui": "30.6.0", + "@elastic/eui": "31.0.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "^9.0.1-kibana3", "@elastic/node-crypto": "1.2.1", @@ -824,7 +824,7 @@ "url-loader": "^2.2.0", "use-resize-observer": "^6.0.0", "val-loader": "^1.1.1", - "vega": "^5.17.1", + "vega": "^5.17.3", "vega-lite": "^4.17.0", "vega-schema-url-parser": "^2.1.0", "vega-tooltip": "^0.24.2", diff --git a/packages/kbn-apm-config-loader/src/config.ts b/packages/kbn-apm-config-loader/src/config.ts index 6e5a830d04b17..5e3d52cfd27d1 100644 --- a/packages/kbn-apm-config-loader/src/config.ts +++ b/packages/kbn-apm-config-loader/src/config.ts @@ -153,8 +153,8 @@ export class ApmConfiguration { return { globalLabels: { - branch: process.env.ghprbSourceBranch || '', - targetBranch: process.env.ghprbTargetBranch || '', + branch: process.env.GIT_BRANCH || '', + targetBranch: process.env.PR_TARGET_BRANCH || '', ciBuildNumber: process.env.BUILD_NUMBER || '', isPr: process.env.GITHUB_PR_NUMBER ? true : false, prId: process.env.GITHUB_PR_NUMBER || '', diff --git a/src/core/public/chrome/ui/header/_index.scss b/src/core/public/chrome/ui/header/_index.scss index 44cd864278325..b11e7e47f4ae7 100644 --- a/src/core/public/chrome/ui/header/_index.scss +++ b/src/core/public/chrome/ui/header/_index.scss @@ -1,5 +1,19 @@ @include euiHeaderAffordForFixed; +.euiDataGrid__restrictBody { + .headerGlobalNav, + .kbnQueryBar { + display: none; + } +} + +.euiDataGrid__restrictBody.euiBody--headerIsFixed { + .euiFlyout { + top: 0; + height: 100%; + } +} + .chrHeaderHelpMenu__version { text-transform: none; } diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index 2bd296fe338ab..61a9b5f04b23f 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -195,7 +195,13 @@ export class HttpConfig { rawExternalUrlConfig: ExternalUrlConfig ) { this.autoListen = rawHttpConfig.autoListen; - this.host = rawHttpConfig.host; + // TODO: Consider dropping support for '0' in v8.0.0. This value is passed + // to hapi, which validates it. Prior to hapi v20, '0' was considered a + // valid host, however the validation logic internally in hapi was + // re-written for v20 and hapi no longer considers '0' a valid host. For + // details, see: + // https://github.com/elastic/kibana/issues/86716#issuecomment-749623781 + this.host = rawHttpConfig.host === '0' ? '0.0.0.0' : rawHttpConfig.host; this.port = rawHttpConfig.port; this.cors = rawHttpConfig.cors; this.customResponseHeaders = Object.entries(rawHttpConfig.customResponseHeaders ?? {}).reduce( diff --git a/src/dev/run_find_plugins_with_circular_deps.ts b/src/dev/run_find_plugins_with_circular_deps.ts index 9fa2d28b8d5c5..5afb8df8502df 100644 --- a/src/dev/run_find_plugins_with_circular_deps.ts +++ b/src/dev/run_find_plugins_with_circular_deps.ts @@ -31,7 +31,6 @@ interface Options { type CircularDepList = Set; const allowedList: CircularDepList = new Set([ - 'src/plugins/charts -> src/plugins/discover', 'src/plugins/vis_default_editor -> src/plugins/visualizations', 'src/plugins/visualizations -> src/plugins/visualize', 'x-pack/plugins/actions -> x-pack/plugins/case', diff --git a/src/plugins/charts/public/services/mapped_colors/mapped_colors.ts b/src/plugins/charts/public/services/mapped_colors/mapped_colors.ts index 2934d4208d22c..7848cdd3f3140 100644 --- a/src/plugins/charts/public/services/mapped_colors/mapped_colors.ts +++ b/src/plugins/charts/public/services/mapped_colors/mapped_colors.ts @@ -37,15 +37,15 @@ export class MappedColors { private _mapping: any; constructor( - private uiSettings: CoreSetup['uiSettings'], + private uiSettings?: CoreSetup['uiSettings'], private colorPaletteFn: (num: number) => string[] = createColorPalette ) { this._oldMap = {}; this._mapping = {}; } - private getConfigColorMapping() { - return _.mapValues(this.uiSettings.get(COLOR_MAPPING_SETTING), standardizeColor); + private getConfigColorMapping(): Record { + return _.mapValues(this.uiSettings?.get(COLOR_MAPPING_SETTING) || {}, standardizeColor); } public get oldMap(): any { diff --git a/src/plugins/charts/public/services/palettes/palettes.test.tsx b/src/plugins/charts/public/services/palettes/palettes.test.tsx index 5d9337f1ee683..7356f13fddf9f 100644 --- a/src/plugins/charts/public/services/palettes/palettes.test.tsx +++ b/src/plugins/charts/public/services/palettes/palettes.test.tsx @@ -18,9 +18,11 @@ */ import { coreMock } from '../../../../../core/public/mocks'; +import { createColorPalette as createLegacyColorPalette } from '../../../../../../src/plugins/charts/public'; import { PaletteDefinition } from './types'; import { buildPalettes } from './palettes'; import { colorsServiceMock } from '../legacy_colors/mock'; +import { euiPaletteColorBlind, euiPaletteColorBlindBehindText } from '@elastic/eui'; describe('palettes', () => { const palettes: Record = buildPalettes( @@ -28,79 +30,257 @@ describe('palettes', () => { colorsServiceMock ); describe('default palette', () => { - it('should return different colors based on behind text flag', () => { - const palette = palettes.default; + describe('syncColors: false', () => { + it('should return different colors based on behind text flag', () => { + const palette = palettes.default; - const color1 = palette.getColor([ - { - name: 'abc', - rankAtDepth: 0, - totalSeriesAtDepth: 5, - }, - ]); - const color2 = palette.getColor( - [ + const color1 = palette.getColor([ { name: 'abc', rankAtDepth: 0, totalSeriesAtDepth: 5, }, - ], - { - behindText: true, - } - ); - expect(color1).not.toEqual(color2); - }); + ]); + const color2 = palette.getColor( + [ + { + name: 'abc', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + ], + { + behindText: true, + } + ); + expect(color1).not.toEqual(color2); + }); - it('should return different colors based on rank at current series', () => { - const palette = palettes.default; + it('should return different colors based on rank at current series', () => { + const palette = palettes.default; - const color1 = palette.getColor([ - { - name: 'abc', - rankAtDepth: 0, - totalSeriesAtDepth: 5, - }, - ]); - const color2 = palette.getColor([ - { - name: 'abc', - rankAtDepth: 1, - totalSeriesAtDepth: 5, - }, - ]); - expect(color1).not.toEqual(color2); + const color1 = palette.getColor([ + { + name: 'abc', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + ]); + const color2 = palette.getColor([ + { + name: 'abc', + rankAtDepth: 1, + totalSeriesAtDepth: 5, + }, + ]); + expect(color1).not.toEqual(color2); + }); + + it('should return the same color for different positions on outer series layers', () => { + const palette = palettes.default; + + const color1 = palette.getColor([ + { + name: 'abc', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + { + name: 'def', + rankAtDepth: 0, + totalSeriesAtDepth: 2, + }, + ]); + const color2 = palette.getColor([ + { + name: 'abc', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + { + name: 'ghj', + rankAtDepth: 1, + totalSeriesAtDepth: 1, + }, + ]); + expect(color1).toEqual(color2); + }); }); - it('should return the same color for different positions on outer series layers', () => { - const palette = palettes.default; + describe('syncColors: true', () => { + it('should return different colors based on behind text flag', () => { + const palette = palettes.default; - const color1 = palette.getColor([ - { - name: 'abc', - rankAtDepth: 0, - totalSeriesAtDepth: 5, - }, - { - name: 'def', - rankAtDepth: 0, - totalSeriesAtDepth: 2, - }, - ]); - const color2 = palette.getColor([ - { - name: 'abc', - rankAtDepth: 0, - totalSeriesAtDepth: 5, - }, - { - name: 'ghj', - rankAtDepth: 1, - totalSeriesAtDepth: 1, - }, - ]); - expect(color1).toEqual(color2); + const color1 = palette.getColor( + [ + { + name: 'abc', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + ], + { + syncColors: true, + } + ); + const color2 = palette.getColor( + [ + { + name: 'abc', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + ], + { + behindText: true, + syncColors: true, + } + ); + expect(color1).not.toEqual(color2); + }); + + it('should return different colors for different keys', () => { + const palette = palettes.default; + + const color1 = palette.getColor( + [ + { + name: 'abc', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + ], + { + syncColors: true, + } + ); + const color2 = palette.getColor( + [ + { + name: 'def', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + ], + { + syncColors: true, + } + ); + expect(color1).not.toEqual(color2); + }); + + it('should return the same color for the same key, irregardless of rank', () => { + const palette = palettes.default; + + const color1 = palette.getColor( + [ + { + name: 'hij', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + ], + { + syncColors: true, + } + ); + const color2 = palette.getColor( + [ + { + name: 'hij', + rankAtDepth: 5, + totalSeriesAtDepth: 5, + }, + ], + { + syncColors: true, + } + ); + expect(color1).toEqual(color2); + }); + + it('should return the same color for different positions on outer series layers', () => { + const palette = palettes.default; + + const color1 = palette.getColor( + [ + { + name: 'klm', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + { + name: 'def', + rankAtDepth: 0, + totalSeriesAtDepth: 2, + }, + ], + { + syncColors: true, + } + ); + const color2 = palette.getColor( + [ + { + name: 'klm', + rankAtDepth: 3, + totalSeriesAtDepth: 5, + }, + { + name: 'ghj', + rankAtDepth: 1, + totalSeriesAtDepth: 1, + }, + ], + { + syncColors: true, + } + ); + expect(color1).toEqual(color2); + }); + + it('should return the same index of the behind text palette for same key', () => { + const palette = palettes.default; + + const color1 = palette.getColor( + [ + { + name: 'klm', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + { + name: 'def', + rankAtDepth: 0, + totalSeriesAtDepth: 2, + }, + ], + { + syncColors: true, + } + ); + const color2 = palette.getColor( + [ + { + name: 'klm', + rankAtDepth: 3, + totalSeriesAtDepth: 5, + }, + { + name: 'ghj', + rankAtDepth: 1, + totalSeriesAtDepth: 1, + }, + ], + { + syncColors: true, + behindText: true, + } + ); + const color1Index = euiPaletteColorBlind({ rotations: 2 }).indexOf(color1!); + const color2Index = euiPaletteColorBlindBehindText({ rotations: 2 }).indexOf(color2!); + expect(color1Index).toEqual(color2Index); + }); }); }); @@ -136,35 +316,87 @@ describe('palettes', () => { (colorsServiceMock.mappedColors.get as jest.Mock).mockClear(); }); - it('should query legacy color service', () => { - palette.getColor([ - { - name: 'abc', - rankAtDepth: 0, - totalSeriesAtDepth: 10, - }, - ]); - expect(colorsServiceMock.mappedColors.mapKeys).toHaveBeenCalledWith(['abc']); - expect(colorsServiceMock.mappedColors.get).toHaveBeenCalledWith('abc'); + describe('syncColors: false', () => { + it('should not query legacy color service', () => { + palette.getColor( + [ + { + name: 'abc', + rankAtDepth: 0, + totalSeriesAtDepth: 10, + }, + ], + { + syncColors: false, + } + ); + expect(colorsServiceMock.mappedColors.mapKeys).not.toHaveBeenCalled(); + expect(colorsServiceMock.mappedColors.get).not.toHaveBeenCalled(); + }); + + it('should return a color from the legacy palette based on position of first series', () => { + const result = palette.getColor( + [ + { + name: 'abc', + rankAtDepth: 2, + totalSeriesAtDepth: 10, + }, + { + name: 'def', + rankAtDepth: 0, + totalSeriesAtDepth: 10, + }, + ], + { + syncColors: false, + } + ); + expect(result).toEqual(createLegacyColorPalette(20)[2]); + }); }); - it('should always use root series', () => { - palette.getColor([ - { - name: 'abc', - rankAtDepth: 0, - totalSeriesAtDepth: 10, - }, - { - name: 'def', - rankAtDepth: 0, - totalSeriesAtDepth: 10, - }, - ]); - expect(colorsServiceMock.mappedColors.mapKeys).toHaveBeenCalledTimes(1); - expect(colorsServiceMock.mappedColors.mapKeys).toHaveBeenCalledWith(['abc']); - expect(colorsServiceMock.mappedColors.get).toHaveBeenCalledTimes(1); - expect(colorsServiceMock.mappedColors.get).toHaveBeenCalledWith('abc'); + describe('syncColors: true', () => { + it('should query legacy color service', () => { + palette.getColor( + [ + { + name: 'abc', + rankAtDepth: 0, + totalSeriesAtDepth: 10, + }, + ], + { + syncColors: true, + } + ); + expect(colorsServiceMock.mappedColors.mapKeys).toHaveBeenCalledWith(['abc']); + expect(colorsServiceMock.mappedColors.get).toHaveBeenCalledWith('abc'); + }); + + it('should always use root series', () => { + palette.getColor( + [ + { + name: 'abc', + rankAtDepth: 0, + totalSeriesAtDepth: 10, + }, + { + name: 'def', + rankAtDepth: 0, + totalSeriesAtDepth: 10, + }, + ], + { + syncColors: true, + } + ); + expect(colorsServiceMock.mappedColors.mapKeys).toHaveBeenCalledTimes(1); + expect(colorsServiceMock.mappedColors.mapKeys).toHaveBeenCalledWith(['abc']); + expect(colorsServiceMock.mappedColors.get).toHaveBeenCalledTimes(1); + expect(colorsServiceMock.mappedColors.get).toHaveBeenCalledWith('abc'); + }); }); }); diff --git a/src/plugins/charts/public/services/palettes/palettes.tsx b/src/plugins/charts/public/services/palettes/palettes.tsx index c1fd7c3cc739f..ffb237904b36c 100644 --- a/src/plugins/charts/public/services/palettes/palettes.tsx +++ b/src/plugins/charts/public/services/palettes/palettes.tsx @@ -28,26 +28,45 @@ import { euiPaletteNegative, euiPalettePositive, euiPaletteWarm, - euiPaletteColorBlindBehindText, euiPaletteForStatus, euiPaletteForTemperature, euiPaletteComplimentary, + euiPaletteColorBlindBehindText, } from '@elastic/eui'; -import { ChartsPluginSetup } from '../../../../../../src/plugins/charts/public'; +import { flatten, zip } from 'lodash'; +import { + ChartsPluginSetup, + createColorPalette as createLegacyColorPalette, +} from '../../../../../../src/plugins/charts/public'; import { lightenColor } from './lighten_color'; import { ChartColorConfiguration, PaletteDefinition, SeriesLayer } from './types'; import { LegacyColorsService } from '../legacy_colors'; +import { MappedColors } from '../mapped_colors'; function buildRoundRobinCategoricalWithMappedColors(): Omit { const colors = euiPaletteColorBlind({ rotations: 2 }); const behindTextColors = euiPaletteColorBlindBehindText({ rotations: 2 }); + const behindTextColorMap: Record = Object.fromEntries( + zip(colors, behindTextColors) + ); + const mappedColors = new MappedColors(undefined, (num: number) => { + return flatten(new Array(Math.ceil(num / 10)).fill(colors)).map((color) => color.toLowerCase()); + }); function getColor( series: SeriesLayer[], chartConfiguration: ChartColorConfiguration = { behindText: false } ) { - const outputColor = chartConfiguration.behindText - ? behindTextColors[series[0].rankAtDepth % behindTextColors.length] - : colors[series[0].rankAtDepth % colors.length]; + let outputColor: string; + if (chartConfiguration.syncColors) { + const colorKey = series[0].name; + mappedColors.mapKeys([colorKey]); + const mappedColor = mappedColors.get(colorKey); + outputColor = chartConfiguration.behindText ? behindTextColorMap[mappedColor] : mappedColor; + } else { + outputColor = chartConfiguration.behindText + ? behindTextColors[series[0].rankAtDepth % behindTextColors.length] + : colors[series[0].rankAtDepth % colors.length]; + } if (!chartConfiguration.maxDepth || chartConfiguration.maxDepth === 1) { return outputColor; @@ -115,9 +134,15 @@ function buildGradient( function buildSyncedKibanaPalette( colors: ChartsPluginSetup['legacyColors'] ): Omit { + const staticColors = createLegacyColorPalette(20); function getColor(series: SeriesLayer[], chartConfiguration: ChartColorConfiguration = {}) { - colors.mappedColors.mapKeys([series[0].name]); - const outputColor = colors.mappedColors.get(series[0].name); + let outputColor: string; + if (chartConfiguration.syncColors) { + colors.mappedColors.mapKeys([series[0].name]); + outputColor = colors.mappedColors.get(series[0].name); + } else { + outputColor = staticColors[series[0].rankAtDepth % staticColors.length]; + } if (!chartConfiguration.maxDepth || chartConfiguration.maxDepth === 1) { return outputColor; diff --git a/src/plugins/charts/public/services/palettes/types.ts b/src/plugins/charts/public/services/palettes/types.ts index f92bcb4bd0824..15989578518f5 100644 --- a/src/plugins/charts/public/services/palettes/types.ts +++ b/src/plugins/charts/public/services/palettes/types.ts @@ -55,6 +55,11 @@ export interface ChartColorConfiguration { * adjust colors for better a11y. Might be ignored depending on the palette. */ behindText?: boolean; + /** + * Flag whether a color assignment to a given key should be remembered and re-used the next time the key shows up. + * This setting might be ignored based on the palette. + */ + syncColors?: boolean; } /** diff --git a/src/plugins/dashboard/public/application/dashboard_app_functions.ts b/src/plugins/dashboard/public/application/dashboard_app_functions.ts index 0381fdb2e55b5..af7a485296ea0 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_functions.ts +++ b/src/plugins/dashboard/public/application/dashboard_app_functions.ts @@ -151,6 +151,7 @@ export const getDashboardContainerInput = ({ description: dashboardStateManager.getDescription(), id: dashboardStateManager.savedDashboard.id || '', useMargins: dashboardStateManager.getUseMargins(), + syncColors: dashboardStateManager.getSyncColors(), viewMode: dashboardStateManager.getViewMode(), filters: query.filterManager.getFilters(), query: dashboardStateManager.getQuery(), diff --git a/src/plugins/dashboard/public/application/dashboard_state.test.ts b/src/plugins/dashboard/public/application/dashboard_state.test.ts index b07ea762f35e0..f31ed30f8eb80 100644 --- a/src/plugins/dashboard/public/application/dashboard_state.test.ts +++ b/src/plugins/dashboard/public/application/dashboard_state.test.ts @@ -68,6 +68,7 @@ describe('DashboardState', function () { query: {} as DashboardContainerInput['query'], timeRange: {} as DashboardContainerInput['timeRange'], useMargins: true, + syncColors: false, title: 'ultra awesome test dashboard', isFullScreenMode: false, panels: {} as DashboardContainerInput['panels'], diff --git a/src/plugins/dashboard/public/application/dashboard_state_manager.ts b/src/plugins/dashboard/public/application/dashboard_state_manager.ts index daa0bbdfc9f8a..dfcbfcafd3db1 100644 --- a/src/plugins/dashboard/public/application/dashboard_state_manager.ts +++ b/src/plugins/dashboard/public/application/dashboard_state_manager.ts @@ -404,6 +404,15 @@ export class DashboardStateManager { this.stateContainer.transitions.setOption('useMargins', useMargins); } + public getSyncColors() { + // Existing dashboards that don't define this should default to true. + return this.appState.options.syncColors === undefined ? true : this.appState.options.syncColors; + } + + public setSyncColors(syncColors: boolean) { + this.stateContainer.transitions.setOption('syncColors', syncColors); + } + public getHidePanelTitles() { return this.appState.options.hidePanelTitles; } diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx index 01b4e81fc484c..a3b67ede9f3f9 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx @@ -59,6 +59,7 @@ export interface DashboardContainerInput extends ContainerInput { timeRange: TimeRange; description?: string; useMargins: boolean; + syncColors?: boolean; viewMode: ViewMode; filters: Filter[]; title: string; @@ -93,6 +94,7 @@ export interface InheritedChildInput extends IndexSignature { hidePanelTitles?: boolean; id: string; searchSessionId?: string; + syncColors?: boolean; } export type DashboardReactContextValue = KibanaReactContextValue; @@ -269,6 +271,7 @@ export class DashboardContainer extends Container
{ (async function loadSavedDashboard() { @@ -46,7 +46,7 @@ export const useSavedDashboard = (savedDashboardId: string | undefined, history: pathname: DashboardConstants.CREATE_NEW_DASHBOARD_URL, }); - showWarningToast(getDashboard60Warning()); + toasts.addWarning(getDashboard60Warning()); return; } @@ -63,7 +63,7 @@ export const useSavedDashboard = (savedDashboardId: string | undefined, history: setSavedDashboard(dashboard); } catch (error) { // E.g. a corrupt or deleted dashboard - showDangerToast(error.message); + toasts.addDanger(error.message); history.push(DashboardConstants.LANDING_PAGE_PATH); } })(); @@ -75,8 +75,7 @@ export const useSavedDashboard = (savedDashboardId: string | undefined, history: recentlyAccessedPaths, savedDashboardId, savedDashboards, - showDangerToast, - showWarningToast, + toasts, ]); return savedDashboard; diff --git a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap index fad7d8ddaabfe..bce8a661634f6 100644 --- a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap +++ b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap @@ -126,7 +126,7 @@ exports[`after fetch When given a title that matches multiple dashboards, filter restrictWidth={true} >
@@ -218,7 +218,7 @@ exports[`after fetch hideWriteControls 1`] = ` restrictWidth={true} >
@@ -358,7 +358,7 @@ exports[`after fetch initialFilter 1`] = ` restrictWidth={true} >
@@ -497,7 +497,7 @@ exports[`after fetch renders all table rows 1`] = ` restrictWidth={true} >
@@ -636,7 +636,7 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = ` restrictWidth={true} >
@@ -775,7 +775,7 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = ` restrictWidth={true} >
diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index 915f245fbcd19..87ccbf29b99f7 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -57,10 +57,12 @@ import { showOptionsPopover } from './show_options_popover'; import { TopNavIds } from './top_nav_ids'; import { ShowShareModal } from './show_share_modal'; import { PanelToolbar } from './panel_toolbar'; +import { OverlayRef } from '../../../../../core/public'; import { DashboardContainer } from '..'; export interface DashboardTopNavState { chromeIsVisible: boolean; + addPanelOverlay?: OverlayRef; savedQuery?: SavedQuery; } @@ -111,14 +113,17 @@ export function DashboardTopNav({ const addFromLibrary = useCallback(() => { if (!isErrorEmbeddable(dashboardContainer)) { - openAddPanelFlyout({ - embeddable: dashboardContainer, - getAllFactories: embeddable.getEmbeddableFactories, - getFactory: embeddable.getEmbeddableFactory, - notifications: core.notifications, - overlays: core.overlays, - SavedObjectFinder: getSavedObjectFinder(core.savedObjects, uiSettings), - }); + setState((s) => ({ + ...s, + addPanelOverlay: openAddPanelFlyout({ + embeddable: dashboardContainer, + getAllFactories: embeddable.getEmbeddableFactories, + getFactory: embeddable.getEmbeddableFactory, + notifications: core.notifications, + overlays: core.overlays, + SavedObjectFinder: getSavedObjectFinder(core.savedObjects, uiSettings), + }), + })); } }, [ embeddable.getEmbeddableFactories, @@ -139,8 +144,16 @@ export function DashboardTopNav({ await factory.create({} as EmbeddableInput, dashboardContainer); }, [dashboardContainer, embeddable]); + const clearAddPanel = useCallback(() => { + if (state.addPanelOverlay) { + state.addPanelOverlay.close(); + setState((s) => ({ ...s, addPanelOverlay: undefined })); + } + }, [state.addPanelOverlay]); + const onChangeViewMode = useCallback( (newMode: ViewMode) => { + clearAddPanel(); const isPageRefresh = newMode === dashboardStateManager.getViewMode(); const isLeavingEditMode = !isPageRefresh && newMode === ViewMode.VIEW; const willLoseChanges = isLeavingEditMode && dashboardStateManager.getIsDirty(timefilter); @@ -178,7 +191,7 @@ export function DashboardTopNav({ } }); }, - [redirectTo, timefilter, core.overlays, savedDashboard.id, dashboardStateManager] + [redirectTo, timefilter, core.overlays, savedDashboard.id, dashboardStateManager, clearAddPanel] ); /** @@ -301,8 +314,16 @@ export function DashboardTopNav({ showCopyOnSave={lastDashboardId ? true : false} /> ); + clearAddPanel(); showSaveModal(dashboardSaveModal, core.i18n.Context); - }, [save, core.i18n.Context, savedObjectsTagging, dashboardStateManager, lastDashboardId]); + }, [ + save, + clearAddPanel, + lastDashboardId, + core.i18n.Context, + savedObjectsTagging, + dashboardStateManager, + ]); const runClone = useCallback(() => { const currentTitle = dashboardStateManager.getTitle(); @@ -348,6 +369,10 @@ export function DashboardTopNav({ onUseMarginsChange: (isChecked: boolean) => { dashboardStateManager.setUseMargins(isChecked); }, + syncColors: dashboardStateManager.getSyncColors(), + onSyncColorsChange: (isChecked: boolean) => { + dashboardStateManager.setSyncColors(isChecked); + }, hidePanelTitles: dashboardStateManager.getHidePanelTitles(), onHidePanelTitlesChange: (isChecked: boolean) => { dashboardStateManager.setHidePanelTitles(isChecked); diff --git a/src/plugins/dashboard/public/application/top_nav/options.tsx b/src/plugins/dashboard/public/application/top_nav/options.tsx index 3398696ff40db..86409cdeba74f 100644 --- a/src/plugins/dashboard/public/application/top_nav/options.tsx +++ b/src/plugins/dashboard/public/application/top_nav/options.tsx @@ -27,17 +27,21 @@ interface Props { onUseMarginsChange: (useMargins: boolean) => void; hidePanelTitles: boolean; onHidePanelTitlesChange: (hideTitles: boolean) => void; + syncColors: boolean; + onSyncColorsChange: (syncColors: boolean) => void; } interface State { useMargins: boolean; hidePanelTitles: boolean; + syncColors: boolean; } export class OptionsMenu extends Component { state = { useMargins: this.props.useMargins, hidePanelTitles: this.props.hidePanelTitles, + syncColors: this.props.syncColors, }; constructor(props: Props) { @@ -56,6 +60,12 @@ export class OptionsMenu extends Component { this.setState({ hidePanelTitles: isChecked }); }; + handleSyncColorsChange = (evt: any) => { + const isChecked = evt.target.checked; + this.props.onSyncColorsChange(isChecked); + this.setState({ syncColors: isChecked }); + }; + render() { return ( @@ -80,6 +90,17 @@ export class OptionsMenu extends Component { data-test-subj="dashboardPanelTitlesCheckbox" /> + + + + ); } diff --git a/src/plugins/dashboard/public/application/top_nav/show_options_popover.tsx b/src/plugins/dashboard/public/application/top_nav/show_options_popover.tsx index 7c23e4808fbea..6c519ccad327f 100644 --- a/src/plugins/dashboard/public/application/top_nav/show_options_popover.tsx +++ b/src/plugins/dashboard/public/application/top_nav/show_options_popover.tsx @@ -39,10 +39,14 @@ export function showOptionsPopover({ onUseMarginsChange, hidePanelTitles, onHidePanelTitlesChange, + syncColors, + onSyncColorsChange, }: { anchorElement: HTMLElement; useMargins: boolean; onUseMarginsChange: (useMargins: boolean) => void; + syncColors: boolean; + onSyncColorsChange: (syncColors: boolean) => void; hidePanelTitles: boolean; onHidePanelTitlesChange: (hideTitles: boolean) => void; }) { @@ -62,6 +66,8 @@ export function showOptionsPopover({ onUseMarginsChange={onUseMarginsChange} hidePanelTitles={hidePanelTitles} onHidePanelTitlesChange={onHidePanelTitlesChange} + syncColors={syncColors} + onSyncColorsChange={onSyncColorsChange} /> diff --git a/src/plugins/dashboard/public/types.ts b/src/plugins/dashboard/public/types.ts index 7e859a81d9d4d..882c5b4286263 100644 --- a/src/plugins/dashboard/public/types.ts +++ b/src/plugins/dashboard/public/types.ts @@ -78,6 +78,7 @@ export interface DashboardAppState { options: { hidePanelTitles: boolean; useMargins: boolean; + syncColors?: boolean; }; query: Query | string; filters: Filter[]; diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts index 4334af63539e3..321a102e8d782 100644 --- a/src/plugins/discover/common/index.ts +++ b/src/plugins/discover/common/index.ts @@ -27,4 +27,5 @@ export const FIELDS_LIMIT_SETTING = 'fields:popularLimit'; export const CONTEXT_DEFAULT_SIZE_SETTING = 'context:defaultSize'; export const CONTEXT_STEP_SETTING = 'context:step'; export const CONTEXT_TIE_BREAKER_FIELDS_SETTING = 'context:tieBreakerFields'; +export const DOC_TABLE_LEGACY = 'doc_table:legacy'; export const MODIFY_COLUMNS_ON_SWITCH = 'discover:modifyColumnsOnSwitch'; diff --git a/src/plugins/discover/public/__mocks__/index_pattern.ts b/src/plugins/discover/public/__mocks__/index_pattern.ts index 706118cb71350..f2c12315d4b90 100644 --- a/src/plugins/discover/public/__mocks__/index_pattern.ts +++ b/src/plugins/discover/public/__mocks__/index_pattern.ts @@ -22,29 +22,40 @@ import { IndexPattern } from '../../../data/common'; import { indexPatterns } from '../../../data/public'; const fields = [ + { + name: '_source', + type: '_source', + scripted: false, + filterable: false, + aggregatable: false, + }, { name: '_index', type: 'string', scripted: false, filterable: true, + aggregatable: false, }, { name: 'message', type: 'string', scripted: false, filterable: false, + aggregatable: false, }, { name: 'extension', type: 'string', scripted: false, filterable: true, + aggregatable: true, }, { name: 'bytes', type: 'number', scripted: false, filterable: true, + aggregatable: true, }, { name: 'scripted', @@ -62,16 +73,21 @@ const indexPattern = ({ id: 'the-index-pattern-id', title: 'the-index-pattern-title', metaFields: ['_index', '_score'], + formatField: jest.fn(), flattenHit: undefined, formatHit: jest.fn((hit) => hit._source), fields, - getComputedFields: () => ({}), + getComputedFields: () => ({ docvalueFields: [], scriptFields: {}, storedFields: ['*'] }), getSourceFiltering: () => ({}), getFieldByName: () => ({}), timeFieldName: '', + docvalueFields: [], } as unknown) as IndexPattern; indexPattern.flattenHit = indexPatterns.flattenHitWrapper(indexPattern, indexPattern.metaFields); indexPattern.isTimeBased = () => !!indexPattern.timeFieldName; +indexPattern.formatField = (hit: Record, fieldName: string) => { + return fieldName === '_source' ? hit._source : indexPattern.flattenHit(hit)[fieldName]; +}; export const indexPatternMock = indexPattern; diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 99497d61c716e..639e2212392cc 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -24,7 +24,6 @@ import moment from 'moment'; import dateMath from '@elastic/datemath'; import { i18n } from '@kbn/i18n'; import { createSearchSessionRestorationDataProvider, getState, splitState } from './discover_state'; - import { RequestAdapter } from '../../../../inspector/public'; import { connectToQueryState, @@ -35,6 +34,7 @@ import { import { getSortArray } from './doc_table'; import * as columnActions from './doc_table/actions/columns'; import indexTemplateLegacy from './discover_legacy.html'; +import indexTemplateGrid from './discover_datagrid.html'; import { addHelpMenuToAppChrome } from '../components/help_menu/help_menu_util'; import { discoverResponseHandler } from './response_handler'; import { @@ -124,7 +124,9 @@ app.config(($routeProvider) => { }; const discoverRoute = { ...defaults, - template: indexTemplateLegacy, + template: getServices().uiSettings.get('doc_table:legacy', true) + ? indexTemplateLegacy + : indexTemplateGrid, reloadOnSearch: false, resolve: { savedObjects: function ($route, Promise) { @@ -340,6 +342,8 @@ function discoverController($element, $route, $scope, $timeout, Promise, uiCapab $scope.minimumVisibleRows = 50; $scope.fetchStatus = fetchStatuses.UNINITIALIZED; $scope.showSaveQuery = uiCapabilities.discover.saveQuery; + $scope.showTimeCol = + !config.get('doc_table:hideTimeColumn', false) && $scope.indexPattern.timeFieldName; let abortController; $scope.$on('$destroy', () => { @@ -414,7 +418,7 @@ function discoverController($element, $route, $scope, $timeout, Promise, uiCapab const query = $scope.searchSource.getField('query') || data.query.queryString.getDefaultQuery(); const sort = getSortArray(savedSearch.sort, $scope.indexPattern); - return { + const defaultState = { query, sort: !sort.length ? getDefaultSort($scope.indexPattern, config.get(SORT_DEFAULT_ORDER_SETTING, 'desc')) @@ -427,6 +431,11 @@ function discoverController($element, $route, $scope, $timeout, Promise, uiCapab interval: 'auto', filters: _.cloneDeep($scope.searchSource.getOwnField('filter')), }; + if (savedSearch.grid) { + defaultState.grid = savedSearch.grid; + } + + return defaultState; } $scope.state.index = $scope.indexPattern.id; @@ -440,6 +449,8 @@ function discoverController($element, $route, $scope, $timeout, Promise, uiCapab indexPatternList: $route.current.locals.savedObjects.ip.list, config: config, setHeaderActionMenu: getHeaderActionMenuMounter(), + filterManager, + setAppState, data, }; @@ -783,6 +794,17 @@ function discoverController($element, $route, $scope, $timeout, Promise, uiCapab const columns = columnActions.moveColumn($scope.state.columns, columnName, newIndex); setAppState({ columns }); }; + + $scope.setColumns = function setColumns(columns) { + // remove first element of columns if it's the configured timeFieldName, which is prepended automatically + const actualColumns = + $scope.indexPattern.timeFieldName && $scope.indexPattern.timeFieldName === columns[0] + ? columns.slice(1) + : columns; + $scope.state = { ...$scope.state, columns: actualColumns }; + setAppState({ columns: actualColumns }); + }; + async function setupVisualization() { // If no timefield has been specified we don't create a histogram of messages if (!getTimeField()) return; diff --git a/src/plugins/discover/public/application/angular/discover_datagrid.html b/src/plugins/discover/public/application/angular/discover_datagrid.html new file mode 100644 index 0000000000000..e59ebbb0fafd0 --- /dev/null +++ b/src/plugins/discover/public/application/angular/discover_datagrid.html @@ -0,0 +1,31 @@ + + + + diff --git a/src/plugins/discover/public/application/angular/discover_legacy.html b/src/plugins/discover/public/application/angular/discover_legacy.html index 7cdcd6cbbca3a..3596c0a2519ed 100644 --- a/src/plugins/discover/public/application/angular/discover_legacy.html +++ b/src/plugins/discover/public/application/angular/discover_legacy.html @@ -1,6 +1,5 @@ ( + +)); + +export function DiscoverGridEmbeddable(props: DiscoverGridProps) { + return ( + + + + ); +} + +/** + * this is just needed for the embeddable + */ +export function createDiscoverGridDirective(reactDirective: any) { + return reactDirective(DiscoverGridEmbeddable, [ + ['columns', { watchDepth: 'collection' }], + ['indexPattern', { watchDepth: 'reference' }], + ['onAddColumn', { watchDepth: 'reference', wrapApply: false }], + ['onFilter', { watchDepth: 'reference', wrapApply: false }], + ['onRemoveColumn', { watchDepth: 'reference', wrapApply: false }], + ['onSetColumns', { watchDepth: 'reference', wrapApply: false }], + ['onSort', { watchDepth: 'reference', wrapApply: false }], + ['rows', { watchDepth: 'collection' }], + ['sampleSize', { watchDepth: 'reference' }], + ['searchDescription', { watchDepth: 'reference' }], + ['searchTitle', { watchDepth: 'reference' }], + ['settings', { watchDepth: 'reference' }], + ['showTimeCol', { watchDepth: 'value' }], + ['sort', { watchDepth: 'value' }], + ]); +} diff --git a/src/plugins/discover/public/application/components/create_discover_legacy_directive.ts b/src/plugins/discover/public/application/components/create_discover_legacy_directive.ts index cb3cb06aa90a3..6e5d47be987d8 100644 --- a/src/plugins/discover/public/application/components/create_discover_legacy_directive.ts +++ b/src/plugins/discover/public/application/components/create_discover_legacy_directive.ts @@ -21,7 +21,6 @@ import { DiscoverLegacy } from './discover_legacy'; export function createDiscoverLegacyDirective(reactDirective: any) { return reactDirective(DiscoverLegacy, [ - ['addColumn', { watchDepth: 'reference' }], ['fetch', { watchDepth: 'reference' }], ['fetchCounter', { watchDepth: 'reference' }], ['fetchError', { watchDepth: 'reference' }], @@ -30,6 +29,7 @@ export function createDiscoverLegacyDirective(reactDirective: any) { ['hits', { watchDepth: 'reference' }], ['indexPattern', { watchDepth: 'reference' }], ['minimumVisibleRows', { watchDepth: 'reference' }], + ['onAddColumn', { watchDepth: 'reference' }], ['onAddFilter', { watchDepth: 'reference' }], ['onChangeInterval', { watchDepth: 'reference' }], ['onMoveColumn', { watchDepth: 'reference' }], diff --git a/src/plugins/discover/public/application/components/discover.scss b/src/plugins/discover/public/application/components/discover.scss index b17da97a45930..665bd98c232a5 100644 --- a/src/plugins/discover/public/application/components/discover.scss +++ b/src/plugins/discover/public/application/components/discover.scss @@ -35,6 +35,10 @@ discover-app { } } +.dscPageContent { + border: $euiBorderThin; +} + .dscPageContent, .dscPageContent__inner { height: 100%; @@ -46,6 +50,7 @@ discover-app { .dscResultCount { padding: $euiSizeS; + min-height: $euiSize * 3; @include euiBreakpoint('xs', 's') { .dscResultCount__toggle { @@ -76,6 +81,13 @@ discover-app { padding: $euiSizeS; } +// new slimmer layout for data grid +.dscHistogramGrid { + display: flex; + height: $euiSize * 8; + padding: $euiSizeS $euiSizeS 0 $euiSizeS; +} + .dscTable { // SASSTODO: add a monospace modifier to the doc-table component .kbnDocTable__row { diff --git a/src/plugins/discover/public/application/components/discover.tsx b/src/plugins/discover/public/application/components/discover.tsx new file mode 100644 index 0000000000000..aa756d960e435 --- /dev/null +++ b/src/plugins/discover/public/application/components/discover.tsx @@ -0,0 +1,321 @@ +/* + * 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 './discover.scss'; +import React, { useState, useRef } from 'react'; +import { + EuiButtonEmpty, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiHideFor, + EuiHorizontalRule, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import classNames from 'classnames'; +import { HitsCounter } from './hits_counter'; +import { TimechartHeader } from './timechart_header'; +import { getServices } from '../../kibana_services'; +import { DiscoverUninitialized, DiscoverHistogram } from '../angular/directives'; +import { DiscoverNoResults } from './no_results'; +import { LoadingSpinner } from './loading_spinner/loading_spinner'; +import { search } from '../../../../data/public'; +import { + DiscoverSidebarResponsive, + DiscoverSidebarResponsiveProps, +} from './sidebar/discover_sidebar_responsive'; +import { DiscoverProps } from './discover_legacy'; +import { SortPairArr } from '../angular/doc_table/lib/get_sort'; +import { DiscoverGrid, DiscoverGridProps } from './discover_grid/discover_grid'; + +export const SidebarMemoized = React.memo((props: DiscoverSidebarResponsiveProps) => ( + +)); + +export const DataGridMemoized = React.memo((props: DiscoverGridProps) => ( + +)); + +export function Discover({ + fetch, + fetchCounter, + fetchError, + fieldCounts, + histogramData, + hits, + indexPattern, + onAddColumn, + onAddFilter, + onChangeInterval, + onRemoveColumn, + onSetColumns, + onSort, + opts, + resetQuery, + resultState, + rows, + searchSource, + setIndexPattern, + showSaveQuery, + state, + timefilterUpdateHandler, + timeRange, + topNavMenu, + updateQuery, + updateSavedQueryId, +}: DiscoverProps) { + const scrollableDesktop = useRef(null); + const collapseIcon = useRef(null); + const [toggleOn, toggleChart] = useState(true); + const [isSidebarClosed, setIsSidebarClosed] = useState(false); + const services = getServices(); + const { TopNavMenu } = services.navigation.ui; + const { trackUiMetric } = services; + const { savedSearch, indexPatternList, config } = opts; + const bucketAggConfig = opts.chartAggConfigs?.aggs[1]; + const bucketInterval = + bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig) + ? bucketAggConfig.buckets?.getInterval() + : undefined; + const contentCentered = resultState === 'uninitialized'; + const showTimeCol = !config.get('doc_table:hideTimeColumn', false) && indexPattern.timeFieldName; + const columns = + state.columns && + state.columns.length > 0 && + // check if all columns where removed except the configured timeField (this can't be removed) + !(state.columns.length === 1 && state.columns[0] === indexPattern.timeFieldName) + ? state.columns + : ['_source']; + // if columns include _source this is considered as default view, so you can't remove columns + // until you add a column using Discover's sidebar + const defaultColumns = columns.includes('_source'); + + return ( + + + + +

+ {savedSearch.title} +

+ + + + + + + setIsSidebarClosed(!isSidebarClosed)} + data-test-subj="collapseSideBarButton" + aria-controls="discover-sidebar" + aria-expanded={isSidebarClosed ? 'false' : 'true'} + aria-label={i18n.translate('discover.toggleSidebarAriaLabel', { + defaultMessage: 'Toggle sidebar', + })} + buttonRef={collapseIcon} + /> + + + + + {resultState === 'none' && ( + + )} + {resultState === 'uninitialized' && } + {resultState === 'loading' && } + {resultState === 'ready' && ( + + + + + 0 ? hits : 0} + showResetButton={!!(savedSearch && savedSearch.id)} + onResetQuery={resetQuery} + /> + + {toggleOn && ( + + + + )} + + { + toggleChart(!toggleOn); + }} + > + {toggleOn + ? i18n.translate('discover.hideChart', { + defaultMessage: 'Hide chart', + }) + : i18n.translate('discover.showChart', { + defaultMessage: 'Show chart', + })} + + + + + {toggleOn && opts.timefield && ( + +
+ {opts.chartAggConfigs && histogramData && rows.length !== 0 && ( +
+ +
+ )} +
+ +
+ )} + + + + +
+

+ +

+ {rows && rows.length && ( +
+ { + const grid = { ...state.grid } || {}; + const newColumns = { ...grid.columns } || {}; + newColumns[colSettings.columnId] = { + width: colSettings.width, + }; + const newGrid = { ...grid, columns: newColumns }; + opts.setAppState({ grid: newGrid }); + }} + /> +
+ )} +
+
+
+ )} +
+
+
+
+
+
+ ); +} diff --git a/src/plugins/discover/public/application/components/discover_grid/constants.ts b/src/plugins/discover/public/application/components/discover_grid/constants.ts new file mode 100644 index 0000000000000..dec483da8f8a1 --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/constants.ts @@ -0,0 +1,38 @@ +/* + * 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. + */ +// data types +export const kibanaJSON = 'kibana-json'; +export const geoPoint = 'geo-point'; +export const unknownType = 'unknown'; +export const gridStyle = { + border: 'all', + fontSize: 's', + cellPadding: 's', + rowHover: 'none', +}; + +export const pageSizeArr = [25, 50, 100]; +export const defaultPageSize = 25; +export const toolbarVisibility = { + showColumnSelector: { + allowHide: false, + allowReorder: true, + }, + showStyleSelector: false, +}; diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss b/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss new file mode 100644 index 0000000000000..64a7eda963349 --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss @@ -0,0 +1,68 @@ +.dscDiscoverGrid { + width: 100%; + max-width: 100%; + height: 100%; + overflow: hidden; + + .euiDataGrid__controls { + border: none; + border-bottom: $euiBorderThin; + } + + .euiDataGridRowCell:first-of-type, + .euiDataGrid--headerShade.euiDataGrid--bordersAll .euiDataGridHeaderCell:first-of-type { + border-left: none; + border-right: none; + } + + .euiDataGridRowCell:last-of-type, + .euiDataGridHeaderCell:last-of-type { + border-right: none; + } +} + +.dscDiscoverGrid__footer { + background-color: $euiColorLightShade; + padding: $euiSize / 2 $euiSize; + margin-top: $euiSize / 4; + text-align: center; +} + +.dscTable__flyoutHeader { + white-space: nowrap; +} + +// We only truncate if the cell is not a control column. +.euiDataGridHeader { + .euiDataGridHeaderCell__content { + @include euiTextTruncate; + overflow: hidden; + white-space: nowrap; + flex-grow: 1; + } + + .euiDataGridHeaderCell__popover { + flex-grow: 0; + flex-basis: auto; + width: auto; + padding-left: $euiSizeXS; + } +} + +.euiDataGridRowCell--numeric { + text-align: right; +} + +.euiDataGrid__noResults { + display: flex; + flex-direction: column; + justify-content: center; + flex: 1 0 100%; + text-align: center; + height: 100%; + width: 100%; +} + +.dscFormatSource { + @include euiTextTruncate; +} diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx new file mode 100644 index 0000000000000..9588f74ed2bc2 --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx @@ -0,0 +1,336 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useCallback, useMemo, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import './discover_grid.scss'; +import { + EuiDataGridSorting, + EuiDataGridStyle, + EuiDataGridProps, + EuiDataGrid, + EuiIcon, + EuiScreenReaderOnly, + EuiSpacer, + EuiText, + htmlIdGenerator, +} from '@elastic/eui'; +import { IndexPattern } from '../../../kibana_services'; +import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { getPopoverContents, getSchemaDetectors } from './discover_grid_schema'; +import { DiscoverGridFlyout } from './discover_grid_flyout'; +import { DiscoverGridContext } from './discover_grid_context'; +import { getRenderCellValueFn } from './get_render_cell_value'; +import { DiscoverGridSettings } from './types'; +import { SortPairArr } from '../../angular/doc_table/lib/get_sort'; +import { + getEuiGridColumns, + getLeadControlColumns, + getVisibleColumns, +} from './discover_grid_columns'; +import { defaultPageSize, gridStyle, pageSizeArr, toolbarVisibility } from './constants'; +import { DiscoverServices } from '../../../build_services'; + +interface SortObj { + id: string; + direction: string; +} + +export interface DiscoverGridProps { + /** + * Determines which element labels the grid for ARIA + */ + ariaLabelledBy: string; + /** + * Determines which columns are displayed + */ + columns: string[]; + /** + * Determines whether the given columns are the default ones, so parts of the document + * are displayed (_source) with limited actions (cannor move, remove columns) + * Implemented for matching with legacy behavior + */ + defaultColumns: boolean; + /** + * The used index pattern + */ + indexPattern: IndexPattern; + /** + * Function used to add a column in the document flyout + */ + onAddColumn: (column: string) => void; + /** + * Function to add a filter in the grid cell or document flyout + */ + onFilter: DocViewFilterFn; + /** + * Function used in the grid header and flyout to remove a column + * @param column + */ + onRemoveColumn: (column: string) => void; + /** + * Function triggered when a column is resized by the user + */ + onResize?: (colSettings: { columnId: string; width: number }) => void; + /** + * Function to set all columns + */ + onSetColumns: (columns: string[]) => void; + /** + * function to change sorting of the documents + */ + onSort: (sort: string[][]) => void; + /** + * Array of documents provided by Elasticsearch + */ + rows?: ElasticSearchHit[]; + /** + * The max size of the documents returned by Elasticsearch + */ + sampleSize: number; + /** + * Grid display settings persisted in Elasticsearch (e.g. column width) + */ + settings?: DiscoverGridSettings; + /** + * Saved search description + */ + searchDescription?: string; + /** + * Saved search title + */ + searchTitle?: string; + /** + * Discover plugin services + */ + services: DiscoverServices; + /** + * Determines whether the time columns should be displayed (legacy settings) + */ + showTimeCol: boolean; + /** + * Current sort setting + */ + sort: SortPairArr[]; +} + +export const EuiDataGridMemoized = React.memo((props: EuiDataGridProps) => { + return ; +}); + +export const DiscoverGrid = ({ + ariaLabelledBy, + columns, + defaultColumns, + indexPattern, + onAddColumn, + onFilter, + onRemoveColumn, + onResize, + onSetColumns, + onSort, + rows, + sampleSize, + searchDescription, + searchTitle, + services, + settings, + showTimeCol, + sort, +}: DiscoverGridProps) => { + const [expanded, setExpanded] = useState(undefined); + + /** + * Pagination + */ + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: defaultPageSize }); + const rowCount = useMemo(() => (rows ? rows.length : 0), [rows]); + const pageCount = useMemo(() => Math.ceil(rowCount / pagination.pageSize), [ + rowCount, + pagination, + ]); + const isOnLastPage = pagination.pageIndex === pageCount - 1; + + const paginationObj = useMemo(() => { + const onChangeItemsPerPage = (pageSize: number) => + setPagination((paginationData) => ({ ...paginationData, pageSize })); + + const onChangePage = (pageIndex: number) => + setPagination((paginationData) => ({ ...paginationData, pageIndex })); + + return { + onChangeItemsPerPage, + onChangePage, + pageIndex: pagination.pageIndex > pageCount - 1 ? 0 : pagination.pageIndex, + pageSize: pagination.pageSize, + pageSizeOptions: pageSizeArr, + }; + }, [pagination, pageCount]); + + /** + * Sorting + */ + const sortingColumns = useMemo(() => sort.map(([id, direction]) => ({ id, direction })), [sort]); + + const onTableSort = useCallback( + (sortingColumnsData) => { + onSort(sortingColumnsData.map(({ id, direction }: SortObj) => [id, direction])); + }, + [onSort] + ); + + /** + * Cell rendering + */ + const renderCellValue = useMemo( + () => + getRenderCellValueFn( + indexPattern, + rows, + rows ? rows.map((hit) => indexPattern.flattenHit(hit)) : [] + ), + [rows, indexPattern] + ); + + /** + * Render variables + */ + const showDisclaimer = rowCount === sampleSize && isOnLastPage; + const randomId = useMemo(() => htmlIdGenerator()(), []); + + const euiGridColumns = useMemo( + () => getEuiGridColumns(columns, settings, indexPattern, showTimeCol, defaultColumns), + [columns, indexPattern, showTimeCol, settings, defaultColumns] + ); + const schemaDetectors = useMemo(() => getSchemaDetectors(), []); + const popoverContents = useMemo(() => getPopoverContents(), []); + const columnsVisibility = useMemo( + () => ({ + visibleColumns: getVisibleColumns(columns, indexPattern, showTimeCol) as string[], + setVisibleColumns: (newColumns: string[]) => { + onSetColumns(newColumns); + }, + }), + [columns, indexPattern, showTimeCol, onSetColumns] + ); + const sorting = useMemo(() => ({ columns: sortingColumns, onSort: onTableSort }), [ + sortingColumns, + onTableSort, + ]); + const lead = useMemo(() => getLeadControlColumns(), []); + + if (!rowCount) { + return ( +
+ + + + + +
+ ); + } + + return ( + + <> + { + if (onResize) { + onResize(col); + } + }} + pagination={paginationObj} + popoverContents={popoverContents} + renderCellValue={renderCellValue} + rowCount={rowCount} + schemaDetectors={schemaDetectors} + sorting={sorting as EuiDataGridSorting} + toolbarVisibility={ + defaultColumns + ? { + ...toolbarVisibility, + showColumnSelector: false, + } + : toolbarVisibility + } + /> + + {showDisclaimer && ( +

+ + + + +

+ )} + {searchTitle && ( + +

+ {searchDescription ? ( + + ) : ( + + )} +

+
+ )} + {expanded && ( + setExpanded(undefined)} + services={services} + /> + )} + +
+ ); +}; diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.test.tsx new file mode 100644 index 0000000000000..a85583f66c6fa --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.test.tsx @@ -0,0 +1,80 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import { FilterInBtn, FilterOutBtn } from './discover_grid_cell_actions'; +import { DiscoverGridContext } from './discover_grid_context'; + +import { indexPatternMock } from '../../../__mocks__/index_pattern'; +import { esHits } from '../../../__mocks__/es_hits'; +import { EuiButton } from '@elastic/eui'; + +describe('Discover cell actions ', function () { + it('triggers filter function when FilterInBtn is clicked', async () => { + const contextMock = { + expanded: undefined, + setExpanded: jest.fn(), + rows: esHits, + onFilter: jest.fn(), + indexPattern: indexPatternMock, + isDarkMode: false, + }; + + const component = mountWithIntl( + + } + rowIndex={1} + columnId={'extension'} + isExpanded={false} + closePopover={jest.fn()} + /> + + ); + const button = findTestSubject(component, 'filterForButton'); + await button.simulate('click'); + expect(contextMock.onFilter).toHaveBeenCalledWith('extension', 'jpg', '+'); + }); + it('triggers filter function when FilterOutBtn is clicked', async () => { + const contextMock = { + expanded: undefined, + setExpanded: jest.fn(), + rows: esHits, + onFilter: jest.fn(), + indexPattern: indexPatternMock, + isDarkMode: false, + }; + + const component = mountWithIntl( + + } + rowIndex={1} + columnId={'extension'} + isExpanded={false} + closePopover={jest.fn()} + /> + + ); + const button = findTestSubject(component, 'filterOutButton'); + await button.simulate('click'); + expect(contextMock.onFilter).toHaveBeenCalledWith('extension', 'jpg', '-'); + }); +}); diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.tsx new file mode 100644 index 0000000000000..ef56166258c9b --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.tsx @@ -0,0 +1,97 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useContext } from 'react'; +import { EuiDataGridColumnCellActionProps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { IndexPatternField } from '../../../../../data/common/index_patterns/fields'; +import { DiscoverGridContext } from './discover_grid_context'; + +export const FilterInBtn = ({ + Component, + rowIndex, + columnId, +}: EuiDataGridColumnCellActionProps) => { + const context = useContext(DiscoverGridContext); + const buttonTitle = i18n.translate('discover.grid.filterForAria', { + defaultMessage: 'Filter for this {value}', + values: { value: columnId }, + }); + + return ( + { + const row = context.rows[rowIndex]; + const flattened = context.indexPattern.flattenHit(row); + + if (flattened) { + context.onFilter(columnId, flattened[columnId], '+'); + } + }} + iconType="plusInCircle" + aria-label={buttonTitle} + title={buttonTitle} + data-test-subj="filterForButton" + > + {i18n.translate('discover.grid.filterFor', { + defaultMessage: 'Filter for', + })} + + ); +}; + +export const FilterOutBtn = ({ + Component, + rowIndex, + columnId, +}: EuiDataGridColumnCellActionProps) => { + const context = useContext(DiscoverGridContext); + const buttonTitle = i18n.translate('discover.grid.filterOutAria', { + defaultMessage: 'Filter out this {value}', + values: { value: columnId }, + }); + + return ( + { + const row = context.rows[rowIndex]; + const flattened = context.indexPattern.flattenHit(row); + + if (flattened) { + context.onFilter(columnId, flattened[columnId], '-'); + } + }} + iconType="minusInCircle" + aria-label={buttonTitle} + title={buttonTitle} + data-test-subj="filterOutButton" + > + {i18n.translate('discover.grid.filterOut', { + defaultMessage: 'Filter out', + })} + + ); +}; + +export function buildCellActions(field: IndexPatternField) { + if (!field.aggregatable && !field.searchable) { + return undefined; + } + + return [FilterInBtn, FilterOutBtn]; +} diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx new file mode 100644 index 0000000000000..dad7e1363fdd9 --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx @@ -0,0 +1,154 @@ +/* + * 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 { indexPatternMock } from '../../../__mocks__/index_pattern'; +import { getEuiGridColumns } from './discover_grid_columns'; +import { indexPatternWithTimefieldMock } from '../../../__mocks__/index_pattern_with_timefield'; + +describe('Discover grid columns ', function () { + it('returns eui grid columns without time column', async () => { + const actual = getEuiGridColumns(['extension', 'message'], {}, indexPatternMock, false, false); + expect(actual).toMatchInlineSnapshot(` + Array [ + Object { + "actions": Object { + "showHide": Object { + "iconType": "cross", + "label": "Remove column", + }, + "showMoveLeft": true, + "showMoveRight": true, + }, + "cellActions": undefined, + "display": undefined, + "id": "extension", + "isSortable": undefined, + "schema": "unknown", + }, + Object { + "actions": Object { + "showHide": Object { + "iconType": "cross", + "label": "Remove column", + }, + "showMoveLeft": true, + "showMoveRight": true, + }, + "cellActions": undefined, + "display": undefined, + "id": "message", + "isSortable": undefined, + "schema": "unknown", + }, + ] + `); + }); + it('returns eui grid columns without time column showing default columns', async () => { + const actual = getEuiGridColumns( + ['extension', 'message'], + {}, + indexPatternWithTimefieldMock, + false, + true + ); + expect(actual).toMatchInlineSnapshot(` + Array [ + Object { + "actions": Object { + "showHide": false, + "showMoveLeft": false, + "showMoveRight": false, + }, + "cellActions": undefined, + "display": undefined, + "id": "extension", + "isSortable": undefined, + "schema": "unknown", + }, + Object { + "actions": Object { + "showHide": false, + "showMoveLeft": false, + "showMoveRight": false, + }, + "cellActions": undefined, + "display": undefined, + "id": "message", + "isSortable": undefined, + "schema": "unknown", + }, + ] + `); + }); + it('returns eui grid columns with time column', async () => { + const actual = getEuiGridColumns( + ['extension', 'message'], + {}, + indexPatternWithTimefieldMock, + true, + false + ); + expect(actual).toMatchInlineSnapshot(` + Array [ + Object { + "actions": Object { + "showHide": false, + "showMoveLeft": true, + "showMoveRight": true, + }, + "cellActions": undefined, + "display": "Time (timestamp)", + "id": "timestamp", + "initialWidth": 180, + "isSortable": undefined, + "schema": "unknown", + }, + Object { + "actions": Object { + "showHide": Object { + "iconType": "cross", + "label": "Remove column", + }, + "showMoveLeft": true, + "showMoveRight": true, + }, + "cellActions": undefined, + "display": undefined, + "id": "extension", + "isSortable": undefined, + "schema": "unknown", + }, + Object { + "actions": Object { + "showHide": Object { + "iconType": "cross", + "label": "Remove column", + }, + "showMoveLeft": true, + "showMoveRight": true, + }, + "cellActions": undefined, + "display": undefined, + "id": "message", + "isSortable": undefined, + "schema": "unknown", + }, + ] + `); + }); +}); diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx new file mode 100644 index 0000000000000..1cf9c84405a61 --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx @@ -0,0 +1,122 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiDataGridColumn, EuiScreenReaderOnly } from '@elastic/eui'; +import { ExpandButton } from './discover_grid_expand_button'; +import { DiscoverGridSettings } from './types'; +import { IndexPattern } from '../../../../../data/common/index_patterns/index_patterns'; +import { buildCellActions } from './discover_grid_cell_actions'; +import { getSchemaByKbnType } from './discover_grid_schema'; + +export function getLeadControlColumns() { + return [ + { + id: 'openDetails', + width: 32, + headerCellRender: () => ( + + + {i18n.translate('discover.controlColumnHeader', { + defaultMessage: 'Control column', + })} + + + ), + rowCellRender: ExpandButton, + }, + ]; +} + +export function buildEuiGridColumn( + columnName: string, + columnWidth: number | undefined = 0, + indexPattern: IndexPattern, + defaultColumns: boolean +) { + const timeString = i18n.translate('discover.timeLabel', { + defaultMessage: 'Time', + }); + const indexPatternField = indexPattern.getFieldByName(columnName); + const column: EuiDataGridColumn = { + id: columnName, + schema: getSchemaByKbnType(indexPatternField?.type), + isSortable: indexPatternField?.sortable, + display: indexPatternField?.displayName, + actions: { + showHide: + defaultColumns || columnName === indexPattern.timeFieldName + ? false + : { + label: i18n.translate('discover.removeColumnLabel', { + defaultMessage: 'Remove column', + }), + iconType: 'cross', + }, + showMoveLeft: !defaultColumns, + showMoveRight: !defaultColumns, + }, + cellActions: indexPatternField ? buildCellActions(indexPatternField) : [], + }; + + if (column.id === indexPattern.timeFieldName) { + column.display = `${timeString} (${indexPattern.timeFieldName})`; + column.initialWidth = 180; + } + if (columnWidth > 0) { + column.initialWidth = Number(columnWidth); + } + return column; +} + +export function getEuiGridColumns( + columns: string[], + settings: DiscoverGridSettings | undefined, + indexPattern: IndexPattern, + showTimeCol: boolean, + defaultColumns: boolean +) { + const timeFieldName = indexPattern.timeFieldName; + const getColWidth = (column: string) => settings?.columns?.[column]?.width ?? 0; + + if (showTimeCol && indexPattern.timeFieldName && !columns.find((col) => col === timeFieldName)) { + const usedColumns = [indexPattern.timeFieldName, ...columns]; + return usedColumns.map((column) => + buildEuiGridColumn(column, getColWidth(column), indexPattern, defaultColumns) + ); + } + + return columns.map((column) => + buildEuiGridColumn(column, getColWidth(column), indexPattern, defaultColumns) + ); +} + +export function getVisibleColumns( + columns: string[], + indexPattern: IndexPattern, + showTimeCol: boolean +) { + const timeFieldName = indexPattern.timeFieldName; + + if (showTimeCol && !columns.find((col) => col === timeFieldName)) { + return [timeFieldName, ...columns]; + } + + return columns; +} diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_context.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_context.tsx new file mode 100644 index 0000000000000..dcc404a0e48df --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_context.tsx @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { IndexPattern } from '../../../kibana_services'; + +export interface GridContext { + expanded: ElasticSearchHit | undefined; + setExpanded: (hit: ElasticSearchHit | undefined) => void; + rows: ElasticSearchHit[]; + onFilter: DocViewFilterFn; + indexPattern: IndexPattern; + isDarkMode: boolean; +} + +const defaultContext = ({} as unknown) as GridContext; + +export const DiscoverGridContext = React.createContext(defaultContext); diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.test.tsx new file mode 100644 index 0000000000000..82fcad8c2cd6f --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.test.tsx @@ -0,0 +1,106 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import { ExpandButton } from './discover_grid_expand_button'; +import { DiscoverGridContext } from './discover_grid_context'; +import { indexPatternMock } from '../../../__mocks__/index_pattern'; +import { esHits } from '../../../__mocks__/es_hits'; + +describe('Discover grid view button ', function () { + it('when no document is expanded, setExpanded is called with current document', async () => { + const contextMock = { + expanded: undefined, + setExpanded: jest.fn(), + rows: esHits, + onFilter: jest.fn(), + indexPattern: indexPatternMock, + isDarkMode: false, + }; + + const component = mountWithIntl( + + + + ); + const button = findTestSubject(component, 'docTableExpandToggleColumn'); + await button.simulate('click'); + expect(contextMock.setExpanded).toHaveBeenCalledWith(esHits[0]); + }); + it('when the current document is expanded, setExpanded is called with undefined', async () => { + const contextMock = { + expanded: esHits[0], + setExpanded: jest.fn(), + rows: esHits, + onFilter: jest.fn(), + indexPattern: indexPatternMock, + isDarkMode: false, + }; + + const component = mountWithIntl( + + + + ); + const button = findTestSubject(component, 'docTableExpandToggleColumn'); + await button.simulate('click'); + expect(contextMock.setExpanded).toHaveBeenCalledWith(undefined); + }); + it('when another document is expanded, setExpanded is called with the current document', async () => { + const contextMock = { + expanded: esHits[0], + setExpanded: jest.fn(), + rows: esHits, + onFilter: jest.fn(), + indexPattern: indexPatternMock, + isDarkMode: false, + }; + + const component = mountWithIntl( + + + + ); + const button = findTestSubject(component, 'docTableExpandToggleColumn'); + await button.simulate('click'); + expect(contextMock.setExpanded).toHaveBeenCalledWith(esHits[1]); + }); +}); diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx new file mode 100644 index 0000000000000..d4a3fe85e34ef --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useContext, useEffect } from 'react'; +import { EuiButtonIcon, EuiDataGridCellValueElementProps, EuiToolTip } from '@elastic/eui'; +import themeDark from '@elastic/eui/dist/eui_theme_dark.json'; +import themeLight from '@elastic/eui/dist/eui_theme_light.json'; +import { i18n } from '@kbn/i18n'; +import { DiscoverGridContext } from './discover_grid_context'; +/** + * Button to expand a given row + */ +export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueElementProps) => { + const { expanded, setExpanded, rows, isDarkMode } = useContext(DiscoverGridContext); + const current = rows[rowIndex]; + useEffect(() => { + if (expanded && current && expanded._id === current._id) { + setCellProps({ + style: { + backgroundColor: isDarkMode ? themeDark.euiColorHighlight : themeLight.euiColorHighlight, + }, + }); + } else { + setCellProps({ style: undefined }); + } + }, [expanded, current, setCellProps, isDarkMode]); + + const isCurrentRowExpanded = current === expanded; + const buttonLabel = i18n.translate('discover.grid.viewDoc', { + defaultMessage: 'Toggle dialog with details', + }); + + return ( + + setExpanded(isCurrentRowExpanded ? undefined : current)} + color={isCurrentRowExpanded ? 'primary' : 'subdued'} + iconType={isCurrentRowExpanded ? 'minimize' : 'expand'} + isSelected={isCurrentRowExpanded} + /> + + ); +}; diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx new file mode 100644 index 0000000000000..79ad98ae2babe --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx @@ -0,0 +1,143 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, + EuiButtonEmpty, + EuiText, + EuiSpacer, + EuiPortal, +} from '@elastic/eui'; +import { DocViewer } from '../doc_viewer/doc_viewer'; +import { IndexPattern } from '../../../kibana_services'; +import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { DiscoverServices } from '../../../build_services'; +import { getContextUrl } from '../../helpers/get_context_url'; + +interface Props { + columns: string[]; + hit: ElasticSearchHit; + indexPattern: IndexPattern; + onAddColumn: (column: string) => void; + onClose: () => void; + onFilter: DocViewFilterFn; + onRemoveColumn: (column: string) => void; + services: DiscoverServices; +} + +/** + * Flyout displaying an expanded Elasticsearch document + */ +export function DiscoverGridFlyout({ + hit, + indexPattern, + columns, + onFilter, + onClose, + onRemoveColumn, + onAddColumn, + services, +}: Props) { + return ( + + + + +

+ {i18n.translate('discover.grid.tableRow.detailHeading', { + defaultMessage: 'Expanded document', + })} +

+
+ + + + + + + {i18n.translate('discover.grid.tableRow.viewText', { + defaultMessage: 'View:', + })} + + + + + + {i18n.translate('discover.grid.tableRow.viewSingleDocumentLinkTextSimple', { + defaultMessage: 'Single document', + })} + + + {indexPattern.isTimeBased() && indexPattern.id && ( + + + {i18n.translate('discover.grid.tableRow.viewSurroundingDocumentsLinkTextSimple', { + defaultMessage: 'Surrounding documents', + })} + + + )} + +
+ + { + onFilter(mapping, value, mode); + onClose(); + }} + onRemoveColumn={(columnName: string) => { + onRemoveColumn(columnName); + onClose(); + }} + onAddColumn={(columnName: string) => { + onAddColumn(columnName); + onClose(); + }} + /> + +
+
+ ); +} diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_schema.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_schema.tsx new file mode 100644 index 0000000000000..aa87d3982fa06 --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_schema.tsx @@ -0,0 +1,103 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { ReactNode } from 'react'; +import { EuiCodeBlock } from '@elastic/eui'; +import { geoPoint, kibanaJSON, unknownType } from './constants'; +import { KBN_FIELD_TYPES } from '../../../../../data/common'; + +export function getSchemaByKbnType(kbnType: string | undefined) { + // Default DataGrid schemas: boolean, numeric, datetime, json, currency, string + switch (kbnType) { + case KBN_FIELD_TYPES.IP: + case KBN_FIELD_TYPES.GEO_SHAPE: + case KBN_FIELD_TYPES.NUMBER: + return 'numeric'; + case KBN_FIELD_TYPES.BOOLEAN: + return 'boolean'; + case KBN_FIELD_TYPES.STRING: + return 'string'; + case KBN_FIELD_TYPES.DATE: + return 'datetime'; + case KBN_FIELD_TYPES._SOURCE: + return kibanaJSON; + case KBN_FIELD_TYPES.GEO_POINT: + return geoPoint; + default: + return unknownType; + } +} + +export function getSchemaDetectors() { + return [ + { + type: kibanaJSON, + detector() { + return 0; // this schema is always explicitly defined + }, + sortTextAsc: '', + sortTextDesc: '', + icon: '', + color: '', + }, + { + type: unknownType, + detector() { + return 0; // this schema is always explicitly defined + }, + sortTextAsc: '', + sortTextDesc: '', + icon: '', + color: '', + }, + { + type: geoPoint, + detector() { + return 0; // this schema is always explicitly defined + }, + sortTextAsc: '', + sortTextDesc: '', + icon: 'tokenGeo', + }, + ]; +} + +/** + * Returns custom popover content for certain schemas + */ +export function getPopoverContents() { + return { + [geoPoint]: ({ children }: { children: ReactNode }) => { + return {children}; + }, + [unknownType]: ({ children }: { children: ReactNode }) => { + return ( + + {children} + + ); + }, + [kibanaJSON]: ({ children }: { children: ReactNode }) => { + return ( + + {children} + + ); + }, + }; +} diff --git a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx new file mode 100644 index 0000000000000..d9896f4c53907 --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx @@ -0,0 +1,132 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { shallow } from 'enzyme'; +import { getRenderCellValueFn } from './get_render_cell_value'; +import { indexPatternMock } from '../../../__mocks__/index_pattern'; +const rows = [ + { + _id: '1', + _index: 'test', + _type: 'test', + _score: 1, + _source: { bytes: 100 }, + }, +]; + +describe('Discover grid cell rendering', function () { + it('renders bytes column correctly', () => { + const DiscoverGridCellValue = getRenderCellValueFn( + indexPatternMock, + rows, + rows.map((row) => indexPatternMock.flattenHit(row)) + ); + const component = shallow( + + ); + expect(component.html()).toMatchInlineSnapshot(`"100"`); + }); + it('renders _source column correctly', () => { + const DiscoverGridCellValue = getRenderCellValueFn( + indexPatternMock, + rows, + rows.map((row) => indexPatternMock.flattenHit(row)) + ); + const component = shallow( + + ); + expect(component.html()).toMatchInlineSnapshot( + `"
bytes
100
"` + ); + }); + + it('renders _source column correctly when isDetails is set to true', () => { + const DiscoverGridCellValue = getRenderCellValueFn( + indexPatternMock, + rows, + rows.map((row) => indexPatternMock.flattenHit(row)) + ); + const component = shallow( + + ); + expect(component.html()).toMatchInlineSnapshot(` + "{ + "bytes": 100 + }" + `); + }); + + it('renders correctly when invalid row is given', () => { + const DiscoverGridCellValue = getRenderCellValueFn( + indexPatternMock, + rows, + rows.map((row) => indexPatternMock.flattenHit(row)) + ); + const component = shallow( + + ); + expect(component.html()).toMatchInlineSnapshot(`"-"`); + }); + it('renders correctly when invalid column is given', () => { + const DiscoverGridCellValue = getRenderCellValueFn( + indexPatternMock, + rows, + rows.map((row) => indexPatternMock.flattenHit(row)) + ); + const component = shallow( + + ); + expect(component.html()).toMatchInlineSnapshot(`"-"`); + }); +}); diff --git a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx new file mode 100644 index 0000000000000..2157e778f84db --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx @@ -0,0 +1,116 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { Fragment, useContext, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import themeLight from '@elastic/eui/dist/eui_theme_light.json'; +import themeDark from '@elastic/eui/dist/eui_theme_dark.json'; + +import { + EuiDataGridCellValueElementProps, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, +} from '@elastic/eui'; +import { IndexPattern } from '../../../kibana_services'; +import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { DiscoverGridContext } from './discover_grid_context'; + +export const getRenderCellValueFn = ( + indexPattern: IndexPattern, + rows: ElasticSearchHit[] | undefined, + rowsFlattened: Array> +) => ({ rowIndex, columnId, isDetails, setCellProps }: EuiDataGridCellValueElementProps) => { + const row = rows ? (rows[rowIndex] as Record) : undefined; + const rowFlattened = rowsFlattened + ? (rowsFlattened[rowIndex] as Record) + : undefined; + + const field = indexPattern.fields.getByName(columnId); + const ctx = useContext(DiscoverGridContext); + + useEffect(() => { + if (ctx.expanded && row && ctx.expanded._id === row._id) { + setCellProps({ + style: { + backgroundColor: ctx.isDarkMode + ? themeDark.euiColorHighlight + : themeLight.euiColorHighlight, + }, + }); + } else { + setCellProps({ style: undefined }); + } + }, [ctx, row, setCellProps]); + + if (typeof row === 'undefined' || typeof rowFlattened === 'undefined') { + return -; + } + + if (field && field.type === '_source') { + if (isDetails) { + // nicely formatted JSON for the expanded view + return {JSON.stringify(row[columnId], null, 2)}; + } + const formatted = indexPattern.formatHit(row); + + return ( + + {Object.keys(formatted).map((key) => ( + + {key} + + + ))} + + ); + } + + if (!field?.type && rowFlattened && typeof rowFlattened[columnId] === 'object') { + if (isDetails) { + // nicely formatted JSON for the expanded view + return {JSON.stringify(rowFlattened[columnId], null, 2)}; + } + + return {JSON.stringify(rowFlattened[columnId])}; + } + + if (field?.type === 'geo_point' && rowFlattened && rowFlattened[columnId]) { + const valueFormatted = rowFlattened[columnId] as { lat: number; lon: number }; + return ( +
+ {i18n.translate('discover.latitudeAndLongitude', { + defaultMessage: 'Lat: {lat} Lon: {lon}', + values: { + lat: valueFormatted?.lat, + lon: valueFormatted?.lon, + }, + })} +
+ ); + } + + const valueFormatted = indexPattern.formatField(row, columnId); + if (typeof valueFormatted === 'undefined') { + return -; + } + return ( + // eslint-disable-next-line react/no-danger + + ); +}; diff --git a/src/plugins/discover/public/application/components/discover_grid/types.ts b/src/plugins/discover/public/application/components/discover_grid/types.ts new file mode 100644 index 0000000000000..3d57dbffe924e --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_grid/types.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * User configurable state of data grid, persisted in saved search + */ +export interface DiscoverGridSettings { + columns?: Record; +} + +export interface DiscoverGridSettingsColumn { + width?: number; +} diff --git a/src/plugins/discover/public/application/components/discover_legacy.test.tsx b/src/plugins/discover/public/application/components/discover_legacy.test.tsx index e2f4ba7ab6e2e..bad5c1d2e532d 100644 --- a/src/plugins/discover/public/application/components/discover_legacy.test.tsx +++ b/src/plugins/discover/public/application/components/discover_legacy.test.tsx @@ -67,7 +67,6 @@ function getProps(indexPattern: IndexPattern) { } as unknown) as DiscoverServices; return { - addColumn: jest.fn(), fetch: jest.fn(), fetchCounter: 0, fetchError: undefined, @@ -75,6 +74,7 @@ function getProps(indexPattern: IndexPattern) { hits: esHits.length, indexPattern, minimumVisibleRows: 10, + onAddColumn: jest.fn(), onAddFilter: jest.fn(), onChangeInterval: jest.fn(), onMoveColumn: jest.fn(), diff --git a/src/plugins/discover/public/application/components/discover_legacy.tsx b/src/plugins/discover/public/application/components/discover_legacy.tsx index d228be66990bd..436a145024437 100644 --- a/src/plugins/discover/public/application/components/discover_legacy.tsx +++ b/src/plugins/discover/public/application/components/discover_legacy.tsx @@ -63,46 +63,161 @@ import { import { DocViewFilterFn, ElasticSearchHit } from '../doc_views/doc_views_types'; export interface DiscoverProps { - addColumn: (column: string) => void; + /** + * Function to fetch documents from Elasticsearch + */ fetch: () => void; + /** + * Counter how often data was fetched (used for testing) + */ fetchCounter: number; + /** + * Error in case of a failing document fetch + */ fetchError?: Error; + /** + * Statistics by fields calculated using the fetched documents + */ fieldCounts: Record; + /** + * Histogram aggregation data + */ histogramData?: Chart; + /** + * Number of documents found by recent fetch + */ hits: number; + /** + * Current IndexPattern + */ indexPattern: IndexPattern; + /** + * Value needed for legacy "infinite" loading functionality + * Determins how much records are rendered using the legacy table + * Increased when scrolling down + */ minimumVisibleRows: number; + /** + * Function to add a column to state + */ + onAddColumn: (column: string) => void; + /** + * Function to add a filter to state + */ onAddFilter: DocViewFilterFn; + /** + * Function to change the used time interval of the date histogram + */ onChangeInterval: (interval: string) => void; + /** + * Function to move a given column to a given index, used in legacy table + */ onMoveColumn: (columns: string, newIdx: number) => void; + /** + * Function to remove a given column from state + */ onRemoveColumn: (column: string) => void; + /** + * Function to replace columns in state + */ onSetColumns: (columns: string[]) => void; + /** + * Function to scroll down the legacy table to the bottom + */ onSkipBottomButtonClick: () => void; + /** + * Function to change sorting of the table, triggers a fetch + */ onSort: (sort: string[][]) => void; opts: { + /** + * Date histogram aggregation config + */ chartAggConfigs?: AggConfigs; + /** + * Client of uiSettings + */ config: IUiSettingsClient; + /** + * Data plugin + */ data: DataPublicPluginStart; - fixedScroll: (el: HTMLElement) => void; + /** + * Data plugin filter manager + */ filterManager: FilterManager; + /** + * List of available index patterns + */ indexPatternList: Array>; + /** + * The number of documents that can be displayed in the table/grid + */ sampleSize: number; + /** + * Current instance of SavedSearch + */ savedSearch: SavedSearch; + /** + * Function to set the header menu + */ setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; + /** + * Timefield of the currently used index pattern + */ timefield: string; + /** + * Function to set the current state + */ setAppState: (state: Partial) => void; }; + /** + * Function to reset the current query + */ resetQuery: () => void; + /** + * Current state of the actual query, one of 'uninitialized', 'loading' ,'ready', 'none' + */ resultState: string; + /** + * Array of document of the recent successful search request + */ rows: ElasticSearchHit[]; + /** + * Instance of SearchSource, the high level search API + */ searchSource: ISearchSource; + /** + * Function to change the current index pattern + */ setIndexPattern: (id: string) => void; + /** + * Determines whether the user should be able to use the save query feature + */ showSaveQuery: boolean; + /** + * Current app state of URL + */ state: AppState; + /** + * Function to update the time filter + */ timefilterUpdateHandler: (ranges: { from: number; to: number }) => void; + /** + * Currently selected time range + */ timeRange?: { from: string; to: string }; + /** + * Menu data of top navigation (New, save ...) + */ topNavMenu: TopNavMenuData[]; + /** + * Function to update the actual query + */ updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; + /** + * Function to update the actual savedQuery id + */ updateSavedQueryId: (savedQueryId?: string) => void; } @@ -114,7 +229,6 @@ export const SidebarMemoized = React.memo((props: DiscoverSidebarResponsiveProps )); export function DiscoverLegacy({ - addColumn, fetch, fetchCounter, fieldCounts, @@ -123,6 +237,7 @@ export function DiscoverLegacy({ hits, indexPattern, minimumVisibleRows, + onAddColumn, onAddFilter, onChangeInterval, onMoveColumn, @@ -192,7 +307,7 @@ export function DiscoverLegacy({ fieldCounts={fieldCounts} hits={rows} indexPatternList={indexPatternList} - onAddField={addColumn} + onAddField={onAddColumn} onAddFilter={onAddFilter} onRemoveField={onRemoveColumn} selectedIndexPattern={searchSource && searchSource.getField('index')} @@ -206,6 +321,8 @@ export function DiscoverLegacy({ setIsSidebarClosed(!isSidebarClosed)} data-test-subj="collapseSideBarButton" aria-controls="discover-sidebar" @@ -335,7 +452,7 @@ export function DiscoverLegacy({ sort={state.sort || []} searchDescription={opts.savedSearch.description} searchTitle={opts.savedSearch.lastSavedTitle} - onAddColumn={addColumn} + onAddColumn={onAddColumn} onFilter={onAddFilter} onMoveColumn={onMoveColumn} onRemoveColumn={onRemoveColumn} diff --git a/src/plugins/discover/public/application/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap b/src/plugins/discover/public/application/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap index b5bd961037e21..d02b484a06a49 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap +++ b/src/plugins/discover/public/application/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap @@ -6,6 +6,7 @@ exports[`Render with 3 different tabs 1`] = ` > - +
); } diff --git a/src/plugins/discover/public/application/components/field_name/__snapshots__/field_name.test.tsx.snap b/src/plugins/discover/public/application/components/field_name/__snapshots__/field_name.test.tsx.snap index 2fa96f9372380..6b5e45f8a0448 100644 --- a/src/plugins/discover/public/application/components/field_name/__snapshots__/field_name.test.tsx.snap +++ b/src/plugins/discover/public/application/components/field_name/__snapshots__/field_name.test.tsx.snap @@ -31,7 +31,7 @@ exports[`FieldName renders a geo field 1`] = `
`; -exports[`FieldName renders a number field by providing a field record, useShortDots is set to false 1`] = ` +exports[`FieldName renders a number field by providing a field record 1`] = `
diff --git a/src/plugins/discover/public/application/components/field_name/field_name.test.tsx b/src/plugins/discover/public/application/components/field_name/field_name.test.tsx index 0deddce1c40a8..248191acf9ab9 100644 --- a/src/plugins/discover/public/application/components/field_name/field_name.test.tsx +++ b/src/plugins/discover/public/application/components/field_name/field_name.test.tsx @@ -27,7 +27,7 @@ test('FieldName renders a string field by providing fieldType and fieldName', () expect(component).toMatchSnapshot(); }); -test('FieldName renders a number field by providing a field record, useShortDots is set to false', () => { +test('FieldName renders a number field by providing a field record', () => { const component = render(); expect(component).toMatchSnapshot(); }); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx index 391e15485f074..0957ee101bd27 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx @@ -56,7 +56,6 @@ function getComponent({ }: { selected?: boolean; showDetails?: boolean; - useShortDots?: boolean; field?: IndexPatternField; }) { const indexPattern = getStubIndexPattern( diff --git a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts index d4670a1e76011..22cacae4c3b45 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts @@ -19,51 +19,58 @@ import { groupFields } from './group_fields'; import { getDefaultFieldFilter } from './field_filter'; +import { IndexPatternField } from '../../../../../../data/common/index_patterns/fields'; -describe('group_fields', function () { - it('should group fields in selected, popular, unpopular group', function () { - const fields = [ - { - name: 'category', - type: 'string', - esTypes: ['text'], - count: 1, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, - { - name: 'currency', - type: 'string', - esTypes: ['keyword'], - count: 0, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, - { - name: 'customer_birth_date', - type: 'date', - esTypes: ['date'], - count: 0, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, - ]; +const fields = [ + { + name: 'category', + type: 'string', + esTypes: ['text'], + count: 1, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'currency', + type: 'string', + esTypes: ['keyword'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'customer_birth_date', + type: 'date', + esTypes: ['date'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, +]; - const fieldCounts = { - category: 1, - currency: 1, - customer_birth_date: 1, - }; +const fieldCounts = { + category: 1, + currency: 1, + customer_birth_date: 1, +}; +describe('group_fields', function () { + it('should group fields in selected, popular, unpopular group', function () { const fieldFilterState = getDefaultFieldFilter(); - const actual = groupFields(fields as any, ['currency'], 5, fieldCounts, fieldFilterState); + const actual = groupFields( + fields as IndexPatternField[], + ['currency'], + 5, + fieldCounts, + fieldFilterState + ); expect(actual).toMatchInlineSnapshot(` Object { "popular": Array [ @@ -111,4 +118,34 @@ describe('group_fields', function () { } `); }); + + it('should sort selected fields by columns order ', function () { + const fieldFilterState = getDefaultFieldFilter(); + + const actual1 = groupFields( + fields as IndexPatternField[], + ['customer_birth_date', 'currency', 'unknown'], + 5, + fieldCounts, + fieldFilterState + ); + expect(actual1.selected.map((field) => field.name)).toEqual([ + 'customer_birth_date', + 'currency', + 'unknown', + ]); + + const actual2 = groupFields( + fields as IndexPatternField[], + ['currency', 'customer_birth_date', 'unknown'], + 5, + fieldCounts, + fieldFilterState + ); + expect(actual2.selected.map((field) => field.name)).toEqual([ + 'currency', + 'customer_birth_date', + 'unknown', + ]); + }); }); diff --git a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx index c6a06618900fd..c34becc97cb93 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx +++ b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx @@ -70,6 +70,15 @@ export function groupFields( result.unpopular.push(field); } } + // add columns, that are not part of the index pattern, to be removeable + for (const column of columns) { + if (!result.selected.find((field) => field.name === column)) { + result.selected.push({ name: column, displayName: column } as IndexPatternField); + } + } + result.selected.sort((a, b) => { + return columns.indexOf(a.name) - columns.indexOf(b.name); + }); return result; } diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index d0c3907d31242..e4a8ab7bc67ff 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -36,6 +36,7 @@ import { import { Container, Embeddable } from '../../../../embeddable/public'; import * as columnActions from '../angular/doc_table/actions/columns'; import searchTemplate from './search_template.html'; +import searchTemplateGrid from './search_template_datagrid.html'; import { ISearchEmbeddable, SearchInput, SearchOutput } from './types'; import { SortOrder } from '../angular/doc_table/components/table_header/helpers'; import { getSortForSearchSource } from '../angular/doc_table'; @@ -49,23 +50,29 @@ import { import { SEARCH_EMBEDDABLE_TYPE } from './constants'; import { SavedSearch } from '../..'; import { SAMPLE_SIZE_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common'; +import { DiscoverGridSettings } from '../components/discover_grid/types'; +import { DiscoverServices } from '../../build_services'; +import { ElasticSearchHit } from '../doc_views/doc_views_types'; import { getDefaultSort } from '../angular/doc_table/lib/get_default_sort'; interface SearchScope extends ng.IScope { columns?: string[]; + settings?: DiscoverGridSettings; description?: string; sort?: SortOrder[]; sharedItemTitle?: string; inspectorAdapters?: Adapters; setSortOrder?: (sortPair: SortOrder[]) => void; + setColumns?: (columns: string[]) => void; removeColumn?: (column: string) => void; addColumn?: (column: string) => void; moveColumn?: (column: string, index: number) => void; filter?: (field: IFieldType, value: string[], operator: string) => void; - hits?: any[]; + hits?: ElasticSearchHit[]; indexPattern?: IndexPattern; totalHitCount?: number; isLoading?: boolean; + showTimeCol?: boolean; } interface SearchEmbeddableConfig { @@ -77,6 +84,7 @@ interface SearchEmbeddableConfig { indexPatterns?: IndexPattern[]; editable: boolean; filterManager: FilterManager; + services: DiscoverServices; } export class SearchEmbeddable @@ -95,6 +103,7 @@ export class SearchEmbeddable public readonly type = SEARCH_EMBEDDABLE_TYPE; private filterManager: FilterManager; private abortController?: AbortController; + private services: DiscoverServices; private prevTimeRange?: TimeRange; private prevFilters?: Filter[]; @@ -111,6 +120,7 @@ export class SearchEmbeddable indexPatterns, editable, filterManager, + services, }: SearchEmbeddableConfig, initialInput: SearchInput, private readonly executeTriggerActions: UiActionsStart['executeTriggerActions'], @@ -128,7 +138,7 @@ export class SearchEmbeddable }, parent ); - + this.services = services; this.filterManager = filterManager; this.savedSearch = savedSearch; this.$rootScope = $rootScope; @@ -138,8 +148,8 @@ export class SearchEmbeddable }; this.initializeSearchScope(); - this.autoRefreshFetchSubscription = getServices() - .timefilter.getAutoRefreshFetch$() + this.autoRefreshFetchSubscription = this.services.timefilter + .getAutoRefreshFetch$() .subscribe(this.fetch); this.subscription = this.getUpdated$().subscribe(() => { @@ -167,7 +177,9 @@ export class SearchEmbeddable if (!this.searchScope) { throw new Error('Search scope not defined'); } - this.searchInstance = this.$compile(searchTemplate)(this.searchScope); + this.searchInstance = this.$compile( + this.services.uiSettings.get('doc_table:legacy', true) ? searchTemplate : searchTemplateGrid + )(this.searchScope); const rootNode = angular.element(domNode); rootNode.append(this.searchInstance); @@ -250,6 +262,15 @@ export class SearchEmbeddable this.updateInput({ columns }); }; + searchScope.setColumns = (columns: string[]) => { + this.updateInput({ columns }); + }; + + if (this.savedSearch.grid) { + searchScope.settings = this.savedSearch.grid; + } + searchScope.showTimeCol = !this.services.uiSettings.get('doc_table:hideTimeColumn', false); + searchScope.filter = async (field, value, operator) => { let filters = esFilters.generateFilters( this.filterManager, @@ -286,13 +307,13 @@ export class SearchEmbeddable if (this.abortController) this.abortController.abort(); this.abortController = new AbortController(); - searchSource.setField('size', getServices().uiSettings.get(SAMPLE_SIZE_SETTING)); + searchSource.setField('size', this.services.uiSettings.get(SAMPLE_SIZE_SETTING)); searchSource.setField( 'sort', getSortForSearchSource( this.searchScope.sort, this.searchScope.indexPattern, - getServices().uiSettings.get(SORT_DEFAULT_ORDER_SETTING) + this.services.uiSettings.get(SORT_DEFAULT_ORDER_SETTING) ) ); diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts b/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts index f61fa361f0c0e..d85476568201f 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts @@ -103,6 +103,7 @@ export class SearchEmbeddableFactory filterManager, editable: getServices().capabilities.discover.save as boolean, indexPatterns: indexPattern ? [indexPattern] : [], + services: getServices(), }, input, executeTriggerActions, diff --git a/src/plugins/discover/public/application/embeddable/search_template.html b/src/plugins/discover/public/application/embeddable/search_template.html index e188d230ea307..be2f5cceac080 100644 --- a/src/plugins/discover/public/application/embeddable/search_template.html +++ b/src/plugins/discover/public/application/embeddable/search_template.html @@ -1,20 +1,20 @@ diff --git a/src/plugins/discover/public/application/embeddable/search_template_datagrid.html b/src/plugins/discover/public/application/embeddable/search_template_datagrid.html new file mode 100644 index 0000000000000..6524783897f8f --- /dev/null +++ b/src/plugins/discover/public/application/embeddable/search_template_datagrid.html @@ -0,0 +1,19 @@ + diff --git a/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts b/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts index 4dec1f75ba322..2ab1b93d6c37e 100644 --- a/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts +++ b/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts @@ -51,7 +51,7 @@ describe('getSharingData', () => { "searchRequest": Object { "body": Object { "_source": Object {}, - "fields": undefined, + "fields": Array [], "query": Object { "bool": Object { "filter": Array [], @@ -68,7 +68,9 @@ describe('getSharingData', () => { }, }, ], - "stored_fields": undefined, + "stored_fields": Array [ + "*", + ], }, "index": "the-index-pattern-title", }, diff --git a/src/plugins/discover/public/application/helpers/persist_saved_search.ts b/src/plugins/discover/public/application/helpers/persist_saved_search.ts index 8e956eff598f3..8ec2012b5843e 100644 --- a/src/plugins/discover/public/application/helpers/persist_saved_search.ts +++ b/src/plugins/discover/public/application/helpers/persist_saved_search.ts @@ -53,6 +53,9 @@ export async function persistSavedSearch( savedSearch.columns = state.columns || []; savedSearch.sort = (state.sort as SortOrder[]) || []; + if (state.grid) { + savedSearch.grid = state.grid; + } try { const id = await savedSearch.save(saveOptions); diff --git a/src/plugins/discover/public/get_inner_angular.ts b/src/plugins/discover/public/get_inner_angular.ts index 651a26cad755d..2ace65c31cc03 100644 --- a/src/plugins/discover/public/get_inner_angular.ts +++ b/src/plugins/discover/public/get_inner_angular.ts @@ -41,6 +41,7 @@ import { createTableRowDirective } from './application/angular/doc_table/compone import { createPagerFactory } from './application/angular/doc_table/lib/pager/pager_factory'; import { createInfiniteScrollDirective } from './application/angular/doc_table/infinite_scroll'; import { createDocViewerDirective } from './application/angular/doc_viewer'; +import { createDiscoverGridDirective } from './application/components/create_discover_grid_directive'; import { createRenderCompleteDirective } from './application/angular/directives/render_complete'; import { initAngularBootstrap, @@ -55,6 +56,8 @@ import { import { DiscoverStartPlugins } from './plugin'; import { getScopedHistory } from './kibana_services'; import { createDiscoverLegacyDirective } from './application/components/create_discover_legacy_directive'; +import { createDiscoverDirective } from './application/components/create_discover_directive'; + /** * returns the main inner angular module, it contains all the parts of Angular Discover * needs to render, so in the end the current 'kibana' angular module is no longer necessary @@ -136,7 +139,8 @@ export function initializeInnerAngularModule( .config(watchMultiDecorator) .run(registerListenEventListener) .directive('renderComplete', createRenderCompleteDirective) - .directive('discoverLegacy', createDiscoverLegacyDirective); + .directive('discoverLegacy', createDiscoverLegacyDirective) + .directive('discover', createDiscoverDirective); } function createLocalPromiseModule() { @@ -188,6 +192,7 @@ function createDocTableModule() { .directive('kbnTableRow', createTableRowDirective) .directive('toolBarPagerButtons', createToolBarPagerButtonsDirective) .directive('kbnInfiniteScroll', createInfiniteScrollDirective) + .directive('discoverGrid', createDiscoverGridDirective) .directive('docViewer', createDocViewerDirective) .directive('contextAppLegacy', createContextAppLegacy); } diff --git a/src/plugins/discover/public/saved_searches/_saved_search.ts b/src/plugins/discover/public/saved_searches/_saved_search.ts index 1ec4549f05d49..8a0ec128b4eb2 100644 --- a/src/plugins/discover/public/saved_searches/_saved_search.ts +++ b/src/plugins/discover/public/saved_searches/_saved_search.ts @@ -26,6 +26,7 @@ export function createSavedSearchClass(savedObjects: SavedObjectsStart) { description: 'text', hits: 'integer', columns: 'keyword', + grid: 'object', sort: 'keyword', version: 'integer', }; @@ -45,6 +46,7 @@ export function createSavedSearchClass(savedObjects: SavedObjectsStart) { description: 'text', hits: 'integer', columns: 'keyword', + grid: 'object', sort: 'keyword', version: 'integer', }, diff --git a/src/plugins/discover/public/saved_searches/types.ts b/src/plugins/discover/public/saved_searches/types.ts index d5e5dd765a364..7f6f1a2553d5e 100644 --- a/src/plugins/discover/public/saved_searches/types.ts +++ b/src/plugins/discover/public/saved_searches/types.ts @@ -19,6 +19,7 @@ import { SearchSource } from '../../../data/public'; import { SavedObjectSaveOpts } from '../../../saved_objects/public'; +import { DiscoverGridSettings } from '../application/components/discover_grid/types'; export type SortOrder = [string, string]; export interface SavedSearch { @@ -28,6 +29,7 @@ export interface SavedSearch { description?: string; columns: string[]; sort: SortOrder[]; + grid: DiscoverGridSettings; destroy: () => void; save: (saveOptions: SavedObjectSaveOpts) => Promise; lastSavedTitle?: string; diff --git a/src/plugins/discover/server/saved_objects/search.ts b/src/plugins/discover/server/saved_objects/search.ts index a6e42f956a025..d124a24b120fd 100644 --- a/src/plugins/discover/server/saved_objects/search.ts +++ b/src/plugins/discover/server/saved_objects/search.ts @@ -53,6 +53,7 @@ export const searchSavedObjectType: SavedObjectsType = { }, sort: { type: 'keyword', index: false, doc_values: false }, title: { type: 'text' }, + grid: { type: 'object', enabled: false }, version: { type: 'integer' }, }, }, diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts index f45281ee62202..425928385e64a 100644 --- a/src/plugins/discover/server/ui_settings.ts +++ b/src/plugins/discover/server/ui_settings.ts @@ -33,6 +33,7 @@ import { CONTEXT_DEFAULT_SIZE_SETTING, CONTEXT_STEP_SETTING, CONTEXT_TIE_BREAKER_FIELDS_SETTING, + DOC_TABLE_LEGACY, MODIFY_COLUMNS_ON_SWITCH, } from '../common'; @@ -165,6 +166,23 @@ export const uiSettings: Record = { category: ['discover'], schema: schema.arrayOf(schema.string()), }, + [DOC_TABLE_LEGACY]: { + name: i18n.translate('discover.advancedSettings.docTableVersionName', { + defaultMessage: 'Use legacy table', + }), + value: true, + description: i18n.translate('discover.advancedSettings.docTableVersionDescription', { + defaultMessage: + 'Discover uses a new table layout that includes better data sorting, drag-and-drop columns, and a full screen ' + + 'view. Enable this option if you prefer to fall back to the legacy table.', + }), + category: ['discover'], + schema: schema.boolean(), + metric: { + type: METRIC_TYPE.CLICK, + name: 'discover:useLegacyDataGrid', + }, + }, [MODIFY_COLUMNS_ON_SWITCH]: { name: i18n.translate('discover.advancedSettings.discover.modifyColumnsOnSwitchTitle', { defaultMessage: 'Modify columns when changing index patterns', diff --git a/src/plugins/embeddable/common/types.ts b/src/plugins/embeddable/common/types.ts index d893724f616d2..8366d81a65754 100644 --- a/src/plugins/embeddable/common/types.ts +++ b/src/plugins/embeddable/common/types.ts @@ -55,6 +55,11 @@ export type EmbeddableInput = { * Search session id to group searches */ searchSessionId?: string; + + /** + * Flag whether colors should be synced with other panels + */ + syncColors?: boolean; }; export interface PanelState { diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx index 867092b78ef7a..3363f556b418e 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx @@ -17,20 +17,20 @@ * under the License. */ import React from 'react'; -import { NotificationsStart, OverlayStart } from 'src/core/public'; +import { NotificationsStart, OverlayRef, OverlayStart } from 'src/core/public'; import { EmbeddableStart } from '../../../../../plugin'; import { toMountPoint } from '../../../../../../../kibana_react/public'; import { IContainer } from '../../../../containers'; import { AddPanelFlyout } from './add_panel_flyout'; -export async function openAddPanelFlyout(options: { +export function openAddPanelFlyout(options: { embeddable: IContainer; getFactory: EmbeddableStart['getEmbeddableFactory']; getAllFactories: EmbeddableStart['getEmbeddableFactories']; overlays: OverlayStart; notifications: NotificationsStart; SavedObjectFinder: React.ComponentType; -}) { +}): OverlayRef { const { embeddable, getFactory, @@ -59,4 +59,5 @@ export async function openAddPanelFlyout(options: { ownFocus: true, } ); + return flyoutSession; } diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index a401795c498b3..b20d5866298d5 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -34,6 +34,7 @@ import { MaybePromise } from '@kbn/utility-types'; import { NotificationsStart as NotificationsStart_2 } from 'src/core/public'; import { Observable } from 'rxjs'; import { Optional } from '@kbn/utility-types'; +import { OverlayRef as OverlayRef_2 } from 'src/core/public'; import { OverlayStart as OverlayStart_2 } from 'src/core/public'; import { PackageInfo } from '@kbn/config'; import { Path } from 'history'; @@ -410,6 +411,7 @@ export type EmbeddableInput = { disabledActions?: string[]; disableTriggers?: boolean; searchSessionId?: string; + syncColors?: boolean; }; // Warning: (ae-missing-release-tag) "EmbeddableInstanceConfiguration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -716,7 +718,7 @@ export function openAddPanelFlyout(options: { overlays: OverlayStart_2; notifications: NotificationsStart_2; SavedObjectFinder: React.ComponentType; -}): Promise; +}): OverlayRef_2; // Warning: (ae-missing-release-tag) "OutputSpec" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/src/plugins/expressions/common/expression_renderers/types.ts b/src/plugins/expressions/common/expression_renderers/types.ts index fca1694747ce2..3f3cfb9ed2dd9 100644 --- a/src/plugins/expressions/common/expression_renderers/types.ts +++ b/src/plugins/expressions/common/expression_renderers/types.ts @@ -82,6 +82,7 @@ export interface IInterpreterRenderHandlers { event: (event: any) => void; hasCompatibleActions?: (event: any) => Promise; getRenderMode: () => RenderMode; + isSyncColorsEnabled: () => boolean; /** * This uiState interface is actually `PersistedState` from the visualizations plugin, * but expressions cannot know about vis or it creates a mess of circular dependencies. diff --git a/src/plugins/expressions/public/loader.ts b/src/plugins/expressions/public/loader.ts index e9e0fa18af6c3..1cf499ce2635a 100644 --- a/src/plugins/expressions/public/loader.ts +++ b/src/plugins/expressions/public/loader.ts @@ -64,6 +64,7 @@ export class ExpressionLoader { this.renderHandler = new ExpressionRenderHandler(element, { onRenderError: params && params.onRenderError, renderMode: params?.renderMode, + syncColors: params?.syncColors, hasCompatibleActions: params?.hasCompatibleActions, }); this.render$ = this.renderHandler.render$; diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 404df2db019a1..5c018adc0131b 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -531,7 +531,7 @@ export interface ExpressionRenderError extends Error { // @public (undocumented) export class ExpressionRenderHandler { // Warning: (ae-forgotten-export) The symbol "ExpressionRenderHandlerParams" needs to be exported by the entry point index.d.ts - constructor(element: HTMLElement, { onRenderError, renderMode, hasCompatibleActions, }?: ExpressionRenderHandlerParams); + constructor(element: HTMLElement, { onRenderError, renderMode, syncColors, hasCompatibleActions, }?: ExpressionRenderHandlerParams); // (undocumented) destroy: () => void; // (undocumented) @@ -903,6 +903,8 @@ export interface IExpressionLoaderParams { // (undocumented) searchSessionId?: string; // (undocumented) + syncColors?: boolean; + // (undocumented) uiState?: unknown; // (undocumented) variables?: Record; @@ -920,6 +922,8 @@ export interface IInterpreterRenderHandlers { // (undocumented) hasCompatibleActions?: (event: any) => Promise; // (undocumented) + isSyncColorsEnabled: () => boolean; + // (undocumented) onDestroy: (fn: () => void) => void; // (undocumented) reload: () => void; diff --git a/src/plugins/expressions/public/react_expression_renderer.test.tsx b/src/plugins/expressions/public/react_expression_renderer.test.tsx index 4ebd626e70fc3..ac6fcab33acbf 100644 --- a/src/plugins/expressions/public/react_expression_renderer.test.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.test.tsx @@ -304,4 +304,22 @@ describe('ExpressionRenderer', () => { expect(onEvent).toHaveBeenCalledTimes(1); expect(onEvent.mock.calls[0][0]).toBe(event); }); + + it('should correctly assign classes to the wrapper node', () => { + (ExpressionLoader as jest.Mock).mockImplementation(() => { + return { + render$: new Subject(), + data$: new Subject(), + loading$: new Subject(), + update: jest.fn(), + destroy: jest.fn(), + }; + }); + + const instance = mount(); + // Counte is 2 because the class is applied to ReactExpressionRenderer + internal component + expect(instance.find('.myClassName').length).toBe(2); + + instance.unmount(); + }); }); diff --git a/src/plugins/expressions/public/react_expression_renderer.tsx b/src/plugins/expressions/public/react_expression_renderer.tsx index eac2371ec66d0..3227b34dcc1ff 100644 --- a/src/plugins/expressions/public/react_expression_renderer.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.tsx @@ -170,7 +170,12 @@ export const ReactExpressionRenderer = ({ errorRenderHandlerRef.current = null; }; - }, [hasCustomRenderErrorHandler, onEvent]); + }, [ + hasCustomRenderErrorHandler, + onEvent, + expressionLoaderOptions.renderMode, + expressionLoaderOptions.syncColors, + ]); useEffect(() => { const subscription = reload$?.subscribe(() => { @@ -206,10 +211,9 @@ export const ReactExpressionRenderer = ({ } }, [state.error]); - const classes = classNames('expExpressionRenderer', { + const classes = classNames('expExpressionRenderer', className, { 'expExpressionRenderer-isEmpty': state.isEmpty, 'expExpressionRenderer-hasError': !!state.error, - className, }); const expressionStyles: React.CSSProperties = {}; diff --git a/src/plugins/expressions/public/render.ts b/src/plugins/expressions/public/render.ts index 717776a2861b4..e3091b908deca 100644 --- a/src/plugins/expressions/public/render.ts +++ b/src/plugins/expressions/public/render.ts @@ -31,6 +31,7 @@ export type IExpressionRendererExtraHandlers = Record; export interface ExpressionRenderHandlerParams { onRenderError?: RenderErrorHandlerFnType; renderMode?: RenderMode; + syncColors?: boolean; hasCompatibleActions?: (event: ExpressionRendererEvent) => Promise; } @@ -63,6 +64,7 @@ export class ExpressionRenderHandler { { onRenderError, renderMode, + syncColors, hasCompatibleActions = async () => false, }: ExpressionRenderHandlerParams = {} ) { @@ -101,6 +103,9 @@ export class ExpressionRenderHandler { getRenderMode: () => { return renderMode || 'display'; }, + isSyncColorsEnabled: () => { + return syncColors || false; + }, hasCompatibleActions, }; } diff --git a/src/plugins/expressions/public/types/index.ts b/src/plugins/expressions/public/types/index.ts index f37107abbb716..d709d8ca96bbd 100644 --- a/src/plugins/expressions/public/types/index.ts +++ b/src/plugins/expressions/public/types/index.ts @@ -57,6 +57,7 @@ export interface IExpressionLoaderParams { onRenderError?: RenderErrorHandlerFnType; searchSessionId?: string; renderMode?: RenderMode; + syncColors?: boolean; hasCompatibleActions?: ExpressionRenderHandlerParams['hasCompatibleActions']; } diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index 8b8678371dd83..71199560ee0c7 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -737,6 +737,8 @@ export interface IInterpreterRenderHandlers { // (undocumented) hasCompatibleActions?: (event: any) => Promise; // (undocumented) + isSyncColorsEnabled: () => boolean; + // (undocumented) onDestroy: (fn: () => void) => void; // (undocumented) reload: () => void; diff --git a/src/plugins/home/server/services/sample_data/usage/collector_fetch.test.ts b/src/plugins/home/server/services/sample_data/usage/collector_fetch.test.ts index 54fed3db1de4d..58bb037f8d614 100644 --- a/src/plugins/home/server/services/sample_data/usage/collector_fetch.test.ts +++ b/src/plugins/home/server/services/sample_data/usage/collector_fetch.test.ts @@ -23,7 +23,7 @@ import { fetchProvider } from './collector_fetch'; const getMockFetchClients = (hits?: unknown[]) => { const fetchParamsMock = createCollectorFetchContextMock(); - fetchParamsMock.callCluster.mockResolvedValue({ hits: { hits } }); + fetchParamsMock.esClient.search = jest.fn().mockResolvedValue({ body: { hits: { hits } } }); return fetchParamsMock; }; diff --git a/src/plugins/home/server/services/sample_data/usage/collector_fetch.ts b/src/plugins/home/server/services/sample_data/usage/collector_fetch.ts index 7df9b14d2efb1..ef958873d9663 100644 --- a/src/plugins/home/server/services/sample_data/usage/collector_fetch.ts +++ b/src/plugins/home/server/services/sample_data/usage/collector_fetch.ts @@ -19,6 +19,7 @@ import { get } from 'lodash'; import moment from 'moment'; +import { SearchResponse } from 'src/core/server'; import { CollectorFetchContext } from '../../../../../usage_collection/server'; interface SearchHit { @@ -41,17 +42,23 @@ export interface TelemetryResponse { last_uninstall_set: string | null; } +type ESResponse = SearchResponse; + export function fetchProvider(index: string) { - return async ({ callCluster }: CollectorFetchContext) => { - const response = await callCluster('search', { - index, - body: { - query: { term: { type: { value: 'sample-data-telemetry' } } }, - _source: { includes: ['sample-data-telemetry', 'type', 'updated_at'] }, + return async ({ esClient }: CollectorFetchContext) => { + const { body: response } = await esClient.search( + { + index, + body: { + query: { term: { type: { value: 'sample-data-telemetry' } } }, + _source: { includes: ['sample-data-telemetry', 'type', 'updated_at'] }, + }, + filter_path: 'hits.hits._id,hits.hits._source', }, - filter_path: 'hits.hits._id,hits.hits._source', - ignore: [404], - }); + { + ignore: [404], + } + ); const getLast = ( dataSet: string, diff --git a/src/plugins/vis_type_vislib/public/vis_wrapper.tsx b/src/plugins/vis_type_vislib/public/vis_wrapper.tsx index b8dbd0f945c32..e2e8a98a9a8b6 100644 --- a/src/plugins/vis_type_vislib/public/vis_wrapper.tsx +++ b/src/plugins/vis_type_vislib/public/vis_wrapper.tsx @@ -61,7 +61,7 @@ const VislibWrapper = ({ core, charts, visData, visConfig, handlers }: VislibWra visController.current?.destroy(); visController.current = null; }; - }, [core, charts, handlers]); + }, [core, charts]); useEffect(updateChart, [updateChart]); diff --git a/src/plugins/vis_type_xy/kibana.json b/src/plugins/vis_type_xy/kibana.json index 14c3ce36bf375..619fa8e71c0dd 100644 --- a/src/plugins/vis_type_xy/kibana.json +++ b/src/plugins/vis_type_xy/kibana.json @@ -3,6 +3,6 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["charts", "data", "expressions", "visualizations"], + "requiredPlugins": ["charts", "data", "expressions", "visualizations", "usageCollection"], "requiredBundles": ["kibanaUtils", "visDefaultEditor"] } diff --git a/src/plugins/vis_type_xy/public/editor/components/options/point_series/elastic_charts_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/point_series/elastic_charts_options.tsx index f40972e86af6b..a3e573741644c 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/point_series/elastic_charts_options.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/point_series/elastic_charts_options.tsx @@ -20,14 +20,17 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; import { SelectOption, SwitchOption } from '../../../../../../vis_default_editor/public'; import { ChartType } from '../../../../../common'; import { VisParams } from '../../../../types'; import { ValidationVisOptionsProps } from '../../common'; +import { getTrackUiMetric } from '../../../../services'; export function ElasticChartsOptions(props: ValidationVisOptionsProps) { + const trackUiMetric = getTrackUiMetric(); const { stateParams, setValue, vis, aggs } = props; const hasLineChart = stateParams.seriesParams.some( @@ -49,7 +52,12 @@ export function ElasticChartsOptions(props: ValidationVisOptionsProps })} paramName="detailedTooltip" value={stateParams.detailedTooltip} - setValue={setValue} + setValue={(paramName, value) => { + if (trackUiMetric) { + trackUiMetric(METRIC_TYPE.CLICK, 'detailed_tooltip_switched'); + } + setValue(paramName, value); + }} /> {hasLineChart && ( @@ -61,7 +69,12 @@ export function ElasticChartsOptions(props: ValidationVisOptionsProps options={vis.type.editorConfig.collections.fittingFunctions} paramName="fittingFunction" value={stateParams.fittingFunction} - setValue={setValue} + setValue={(paramName, value) => { + if (trackUiMetric) { + trackUiMetric(METRIC_TYPE.CLICK, 'fitting_function_selected'); + } + setValue(paramName, value); + }} /> )} diff --git a/src/plugins/vis_type_xy/public/plugin.ts b/src/plugins/vis_type_xy/public/plugin.ts index 07084de67fd21..ab22ae57ebbdf 100644 --- a/src/plugins/vis_type_xy/public/plugin.ts +++ b/src/plugins/vis_type_xy/public/plugin.ts @@ -22,6 +22,7 @@ import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; import { VisualizationsSetup, VisualizationsStart } from '../../visualizations/public'; import { ChartsPluginSetup } from '../../charts/public'; import { DataPublicPluginStart } from '../../data/public'; +import { UsageCollectionSetup } from '../../usage_collection/public'; import { createVisTypeXyVisFn } from './xy_vis_fn'; import { @@ -32,6 +33,7 @@ import { setTimefilter, setUISettings, setDocLinks, + setTrackUiMetric, } from './services'; import { visTypesDefinitions } from './vis_types'; import { LEGACY_CHARTS_LIBRARY } from '../common'; @@ -47,6 +49,7 @@ export interface VisTypeXyPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; charts: ChartsPluginSetup; + usageCollection: UsageCollectionSetup; } /** @internal */ @@ -69,7 +72,7 @@ export class VisTypeXyPlugin > { public async setup( core: VisTypeXyCoreSetup, - { expressions, visualizations, charts }: VisTypeXyPluginSetupDependencies + { expressions, visualizations, charts, usageCollection }: VisTypeXyPluginSetupDependencies ) { if (!core.uiSettings.get(LEGACY_CHARTS_LIBRARY, false)) { setUISettings(core.uiSettings); @@ -81,6 +84,8 @@ export class VisTypeXyPlugin visTypesDefinitions.forEach(visualizations.createBaseVisualization); } + setTrackUiMetric(usageCollection?.reportUiCounter.bind(usageCollection, 'vis_type_xy')); + return {}; } diff --git a/src/plugins/vis_type_xy/public/services.ts b/src/plugins/vis_type_xy/public/services.ts index 5a72759ecff6c..086cab8fb217a 100644 --- a/src/plugins/vis_type_xy/public/services.ts +++ b/src/plugins/vis_type_xy/public/services.ts @@ -17,6 +17,7 @@ * under the License. */ +import { UiCounterMetricType } from '@kbn/analytics'; import { CoreSetup, DocLinksStart } from '../../../core/public'; import { createGetterSetter } from '../../kibana_utils/public'; import { DataPublicPluginStart } from '../../data/public'; @@ -47,3 +48,7 @@ export const [getColorsService, setColorsService] = createGetterSetter< >('xy charts.color'); export const [getDocLinks, setDocLinks] = createGetterSetter('DocLinks'); + +export const [getTrackUiMetric, setTrackUiMetric] = createGetterSetter< + (metricType: UiCounterMetricType, eventName: string | string[]) => void +>('trackUiMetric'); diff --git a/test/functional/apps/dashboard/embeddable_data_grid.ts b/test/functional/apps/dashboard/embeddable_data_grid.ts new file mode 100644 index 0000000000000..067536ab7aa93 --- /dev/null +++ b/test/functional/apps/dashboard/embeddable_data_grid.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const dashboardAddPanel = getService('dashboardAddPanel'); + const filterBar = getService('filterBar'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const find = getService('find'); + const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'timePicker', 'discover']); + + describe('dashboard embeddable data grid', () => { + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.loadIfNeeded('dashboard/current/data'); + await esArchiver.loadIfNeeded('dashboard/current/kibana'); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + 'doc_table:legacy': false, + }); + await PageObjects.common.navigateToApp('dashboard'); + await filterBar.ensureFieldEditorModalIsClosed(); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.timePicker.setDefaultDataRange(); + }); + + describe('saved search filters', function () { + it('are added when a cell filter is clicked', async function () { + await dashboardAddPanel.addSavedSearch('Rendering-Test:-saved-search'); + await find.clickByCssSelector(`[role="gridcell"]:nth-child(2)`); + await find.clickByCssSelector(`[data-test-subj="filterOutButton"]`); + await PageObjects.header.waitUntilLoadingHasFinished(); + await find.clickByCssSelector(`[role="gridcell"]:nth-child(2)`); + await find.clickByCssSelector(`[data-test-subj="filterForButton"]`); + const filterCount = await filterBar.getFilterCount(); + expect(filterCount).to.equal(2); + }); + }); + }); +} diff --git a/test/functional/apps/dashboard/index.ts b/test/functional/apps/dashboard/index.ts index 6fb5f874022a0..43ad1aad5de00 100644 --- a/test/functional/apps/dashboard/index.ts +++ b/test/functional/apps/dashboard/index.ts @@ -54,6 +54,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./empty_dashboard')); loadTestFile(require.resolve('./url_field_formatter')); loadTestFile(require.resolve('./embeddable_rendering')); + loadTestFile(require.resolve('./embeddable_data_grid')); loadTestFile(require.resolve('./create_and_add_embeddables')); loadTestFile(require.resolve('./edit_embeddable_redirects')); loadTestFile(require.resolve('./edit_visualizations')); diff --git a/test/functional/apps/discover/_data_grid.ts b/test/functional/apps/discover/_data_grid.ts new file mode 100644 index 0000000000000..8f62e03518253 --- /dev/null +++ b/test/functional/apps/discover/_data_grid.ts @@ -0,0 +1,67 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; + +export default function ({ + getService, + getPageObjects, +}: { + getService: (service: string) => any; + getPageObjects: (pageObjects: string[]) => any; +}) { + describe('discover data grid tests', function describeDiscoverDataGrid() { + const esArchiver = getService('esArchiver'); + const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); + const kibanaServer = getService('kibanaServer'); + const defaultSettings = { defaultIndex: 'logstash-*', 'doc_table:legacy': false }; + const testSubjects = getService('testSubjects'); + + before(async function () { + await esArchiver.load('discover'); + await esArchiver.loadIfNeeded('logstash_functional'); + await kibanaServer.uiSettings.replace(defaultSettings); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + }); + + after(async function () { + await kibanaServer.uiSettings.replace({ 'doc_table:legacy': true }); + }); + + it('can add fields to the table', async function () { + const getTitles = async () => + (await testSubjects.getVisibleText('dataGridHeader')).replace(/\s|\r?\n|\r/g, ' '); + + expect(await getTitles()).to.be('Time (@timestamp) _source'); + + await PageObjects.discover.clickFieldListItemAdd('bytes'); + expect(await getTitles()).to.be('Time (@timestamp) bytes'); + + await PageObjects.discover.clickFieldListItemAdd('agent'); + expect(await getTitles()).to.be('Time (@timestamp) bytes agent'); + + await PageObjects.discover.clickFieldListItemAdd('bytes'); + expect(await getTitles()).to.be('Time (@timestamp) agent'); + + await PageObjects.discover.clickFieldListItemAdd('agent'); + expect(await getTitles()).to.be('Time (@timestamp) _source'); + }); + }); +} diff --git a/test/functional/apps/discover/_data_grid_context.ts b/test/functional/apps/discover/_data_grid_context.ts new file mode 100644 index 0000000000000..6821b9c69cf7e --- /dev/null +++ b/test/functional/apps/discover/_data_grid_context.ts @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +const TEST_COLUMN_NAMES = ['@message']; +const TEST_FILTER_COLUMN_NAMES = [ + ['extension', 'jpg'], + ['geo.src', 'IN'], +]; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const retry = getService('retry'); + const filterBar = getService('filterBar'); + const dataGrid = getService('dataGrid'); + const docTable = getService('docTable'); + const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'settings']); + const defaultSettings = { defaultIndex: 'logstash-*', 'doc_table:legacy': false }; + const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + + describe('discover data grid context tests', () => { + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await kibanaServer.uiSettings.update(defaultSettings); + await PageObjects.common.navigateToApp('discover'); + + for (const columnName of TEST_COLUMN_NAMES) { + await PageObjects.discover.clickFieldListItemAdd(columnName); + } + + for (const [columnName, value] of TEST_FILTER_COLUMN_NAMES) { + await PageObjects.discover.clickFieldListItem(columnName); + await PageObjects.discover.clickFieldListPlusFilter(columnName, value); + } + }); + after(async () => { + await PageObjects.timePicker.resetDefaultAbsoluteRangeViaUiSettings(); + }); + + it('should open the context view with the selected document as anchor', async () => { + // check the anchor timestamp in the context view + await retry.waitFor('selected document timestamp matches anchor timestamp ', async () => { + // get the timestamp of the first row + const discoverFields = await dataGrid.getFields(); + const firstTimestamp = discoverFields[0][0]; + + // navigate to the context view + await dataGrid.clickRowToggle({ rowIndex: 0 }); + const rowActions = await dataGrid.getRowActions({ rowIndex: 0 }); + await rowActions[1].click(); + // entering the context view (contains the legacy type) + const contextFields = await docTable.getFields(); + const anchorTimestamp = contextFields[0][0]; + return anchorTimestamp === firstTimestamp; + }); + }); + + it('should open the context view with the same columns', async () => { + const columnNames = await docTable.getHeaderFields(); + expect(columnNames).to.eql(['Time', ...TEST_COLUMN_NAMES]); + }); + + it('should open the context view with the filters disabled', async () => { + let disabledFilterCounter = 0; + for (const [columnName, value] of TEST_FILTER_COLUMN_NAMES) { + if (await filterBar.hasFilter(columnName, value, false)) { + disabledFilterCounter++; + } + } + expect(disabledFilterCounter).to.be(TEST_FILTER_COLUMN_NAMES.length); + }); + }); +} diff --git a/test/functional/apps/discover/_data_grid_doc_navigation.ts b/test/functional/apps/discover/_data_grid_doc_navigation.ts new file mode 100644 index 0000000000000..92d9893cab0b6 --- /dev/null +++ b/test/functional/apps/discover/_data_grid_doc_navigation.ts @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const filterBar = getService('filterBar'); + const dataGrid = getService('dataGrid'); + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'context']); + const esArchiver = getService('esArchiver'); + const retry = getService('retry'); + const kibanaServer = getService('kibanaServer'); + const defaultSettings = { defaultIndex: 'logstash-*', 'doc_table:legacy': false }; + + describe('discover data grid doc link', function () { + beforeEach(async function () { + await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.loadIfNeeded('discover'); + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await kibanaServer.uiSettings.update(defaultSettings); + await PageObjects.common.navigateToApp('discover'); + }); + + it('should open the doc view of the selected document', async function () { + // navigate to the doc view + await dataGrid.clickRowToggle({ rowIndex: 0 }); + + // click the open action + await retry.try(async () => { + const rowActions = await dataGrid.getRowActions({ rowIndex: 0 }); + if (!rowActions.length) { + throw new Error('row actions empty, trying again'); + } + await rowActions[0].click(); + }); + + const hasDocHit = await testSubjects.exists('doc-hit'); + expect(hasDocHit).to.be(true); + }); + + it('add filter should create an exists filter if value is null (#7189)', async function () { + await PageObjects.discover.waitUntilSearchingHasFinished(); + // Filter special document + await filterBar.addFilter('agent', 'is', 'Missing/Fields'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + await retry.try(async () => { + // navigate to the doc view + await dataGrid.clickRowToggle({ rowIndex: 0 }); + + const details = await dataGrid.getDetailsRow(); + await dataGrid.addInclusiveFilter(details, 'referer'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + const hasInclusiveFilter = await filterBar.hasFilter( + 'referer', + 'exists', + true, + false, + true + ); + expect(hasInclusiveFilter).to.be(true); + + await dataGrid.clickRowToggle({ rowIndex: 0 }); + const detailsExcluding = await dataGrid.getDetailsRow(); + await dataGrid.removeInclusiveFilter(detailsExcluding, 'referer'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + const hasExcludeFilter = await filterBar.hasFilter('referer', 'exists', true, false, false); + expect(hasExcludeFilter).to.be(true); + }); + }); + }); +} diff --git a/test/functional/apps/discover/_data_grid_doc_table.ts b/test/functional/apps/discover/_data_grid_doc_table.ts new file mode 100644 index 0000000000000..1224823abf048 --- /dev/null +++ b/test/functional/apps/discover/_data_grid_doc_table.ts @@ -0,0 +1,132 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const dataGrid = getService('dataGrid'); + const log = getService('log'); + const retry = getService('retry'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); + const defaultSettings = { + defaultIndex: 'logstash-*', + 'doc_table:legacy': false, + }; + + describe('discover data grid doc table', function describeIndexTests() { + const defaultRowsLimit = 25; + + before(async function () { + log.debug('load kibana index with default index pattern'); + await esArchiver.load('discover'); + await esArchiver.loadIfNeeded('logstash_functional'); + await kibanaServer.uiSettings.replace(defaultSettings); + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await PageObjects.common.navigateToApp('discover'); + }); + + it('should show the first 50 rows by default', async function () { + // with the default range the number of hits is ~14000 + const rows = await dataGrid.getDocTableRows(); + expect(rows.length).to.be(defaultRowsLimit); + }); + + it('should refresh the table content when changing time window', async function () { + const initialRows = await dataGrid.getDocTableRows(); + + const fromTime = 'Sep 20, 2015 @ 23:00:00.000'; + const toTime = 'Sep 20, 2015 @ 23:14:00.000'; + + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + const finalRows = await PageObjects.discover.getDocTableRows(); + expect(finalRows.length).to.be.below(initialRows.length); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + }); + + describe('expand a document row', function () { + const rowToInspect = 1; + + it('should expand the detail row when the toggle arrow is clicked', async function () { + await retry.try(async function () { + await dataGrid.clickRowToggle({ isAnchorRow: false, rowIndex: rowToInspect - 1 }); + const detailsEl = await dataGrid.getDetailsRows(); + const defaultMessageEl = await detailsEl[0].findByTestSubject('docTableRowDetailsTitle'); + expect(defaultMessageEl).to.be.ok(); + await dataGrid.closeFlyout(); + }); + }); + + it('should show the detail panel actions', async function () { + await retry.try(async function () { + await dataGrid.clickRowToggle({ isAnchorRow: false, rowIndex: rowToInspect - 1 }); + const [surroundingActionEl, singleActionEl] = await dataGrid.getRowActions({ + isAnchorRow: false, + rowIndex: rowToInspect - 1, + }); + expect(surroundingActionEl).to.be.ok(); + expect(singleActionEl).to.be.ok(); + await dataGrid.closeFlyout(); + }); + }); + }); + + describe('add and remove columns', function () { + const extraColumns = ['phpmemory', 'ip']; + + afterEach(async function () { + for (const column of extraColumns) { + await PageObjects.discover.clickFieldListItemRemove(column); + await PageObjects.header.waitUntilLoadingHasFinished(); + } + }); + + it('should add more columns to the table', async function () { + for (const column of extraColumns) { + await PageObjects.discover.clearFieldSearchInput(); + await PageObjects.discover.findFieldByName(column); + await PageObjects.discover.clickFieldListItemAdd(column); + await PageObjects.header.waitUntilLoadingHasFinished(); + // test the header now + const header = await dataGrid.getHeaderFields(); + expect(header.join(' ')).to.have.string(column); + } + }); + + it('should remove columns from the table', async function () { + for (const column of extraColumns) { + await PageObjects.discover.clearFieldSearchInput(); + await PageObjects.discover.findFieldByName(column); + await PageObjects.discover.clickFieldListItemAdd(column); + await PageObjects.header.waitUntilLoadingHasFinished(); + } + // remove the second column + await PageObjects.discover.clickFieldListItemAdd(extraColumns[1]); + await PageObjects.header.waitUntilLoadingHasFinished(); + // test that the second column is no longer there + const header = await dataGrid.getHeaderFields(); + expect(header.join(' ')).to.not.have.string(extraColumns[1]); + }); + }); + }); +} diff --git a/test/functional/apps/discover/_data_grid_field_data.ts b/test/functional/apps/discover/_data_grid_field_data.ts new file mode 100644 index 0000000000000..8224f59f7fabf --- /dev/null +++ b/test/functional/apps/discover/_data_grid_field_data.ts @@ -0,0 +1,99 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const retry = getService('retry'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const toasts = getService('toasts'); + const queryBar = getService('queryBar'); + const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']); + const defaultSettings = { defaultIndex: 'logstash-*', 'doc_table:legacy': false }; + const dataGrid = getService('dataGrid'); + + describe('discover data grid field data tests', function describeIndexTests() { + this.tags('includeFirefox'); + before(async function () { + await esArchiver.load('discover'); + await esArchiver.loadIfNeeded('logstash_functional'); + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await kibanaServer.uiSettings.update(defaultSettings); + await PageObjects.common.navigateToApp('discover'); + }); + describe('field data', function () { + it('search php should show the correct hit count', async function () { + const expectedHitCount = '445'; + await retry.try(async function () { + await queryBar.setQuery('php'); + await queryBar.submitQuery(); + const hitCount = await PageObjects.discover.getHitCount(); + expect(hitCount).to.be(expectedHitCount); + }); + }); + + it('the search term should be highlighted in the field data', async function () { + // marks is the style that highlights the text in yellow + const marks = await PageObjects.discover.getMarks(); + expect(marks.length).to.be(25); + expect(marks.indexOf('php')).to.be(0); + }); + + it('search type:apache should show the correct hit count', async function () { + const expectedHitCount = '11,156'; + await queryBar.setQuery('type:apache'); + await queryBar.submitQuery(); + await retry.try(async function tryingForTime() { + const hitCount = await PageObjects.discover.getHitCount(); + expect(hitCount).to.be(expectedHitCount); + }); + }); + + it('doc view should show Time and _source columns', async function () { + const expectedHeader = 'Time (@timestamp) _source'; + const DocHeader = await dataGrid.getHeaderFields(); + expect(DocHeader.join(' ')).to.be(expectedHeader); + }); + + it('doc view should sort ascending', async function () { + const expectedTimeStamp = 'Sep 20, 2015 @ 00:00:00.000'; + await dataGrid.clickDocSortAsc(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + await retry.try(async function tryingForTime() { + const rowData = await dataGrid.getFields(); + expect(rowData[0][0].startsWith(expectedTimeStamp)).to.be.ok(); + }); + }); + + it('a bad syntax query should show an error message', async function () { + const expectedError = + 'Expected ":", "<", "<=", ">", ">=", AND, OR, end of input, ' + + 'whitespace but "(" found.'; + await queryBar.setQuery('xxx(yyy))'); + await queryBar.submitQuery(); + const { message } = await toasts.getErrorToast(); + expect(message).to.contain(expectedError); + await toasts.dismissToast(); + }); + }); + }); +} diff --git a/test/functional/apps/discover/_doc_table.ts b/test/functional/apps/discover/_doc_table.ts index 20fda144b338e..40a6ab31f7d4c 100644 --- a/test/functional/apps/discover/_doc_table.ts +++ b/test/functional/apps/discover/_doc_table.ts @@ -131,13 +131,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should add more columns to the table', async function () { - const [column] = extraColumns; - await PageObjects.discover.findFieldByName(column); - log.debug(`add a ${column} column`); - await PageObjects.discover.clickFieldListItemAdd(column); - await PageObjects.header.waitUntilLoadingHasFinished(); - // test the header now - expect(await PageObjects.discover.getDocHeader()).to.have.string(column); + for (const column of extraColumns) { + await PageObjects.discover.clearFieldSearchInput(); + await PageObjects.discover.findFieldByName(column); + await PageObjects.discover.clickFieldListItemAdd(column); + await PageObjects.header.waitUntilLoadingHasFinished(); + // test the header now + expect(await PageObjects.discover.getDocHeader()).to.have.string(column); + } }); it('should remove columns from the table', async function () { diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts index c13529b7d1b43..450049af66abf 100644 --- a/test/functional/apps/discover/index.ts +++ b/test/functional/apps/discover/index.ts @@ -51,5 +51,10 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_date_nanos')); loadTestFile(require.resolve('./_date_nanos_mixed')); loadTestFile(require.resolve('./_indexpattern_without_timefield')); + loadTestFile(require.resolve('./_data_grid')); + loadTestFile(require.resolve('./_data_grid_context')); + loadTestFile(require.resolve('./_data_grid_field_data')); + loadTestFile(require.resolve('./_data_grid_doc_navigation')); + loadTestFile(require.resolve('./_data_grid_doc_table')); }); } diff --git a/test/functional/services/data_grid.ts b/test/functional/services/data_grid.ts index 209e30d23ca3c..c538d8156103c 100644 --- a/test/functional/services/data_grid.ts +++ b/test/functional/services/data_grid.ts @@ -24,10 +24,15 @@ interface TabbedGridData { columns: string[]; rows: string[][]; } +interface SelectOptions { + isAnchorRow?: boolean; + rowIndex: number; +} -export function DataGridProvider({ getService }: FtrProviderContext) { +export function DataGridProvider({ getService, getPageObjects }: FtrProviderContext) { const find = getService('find'); const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'header']); class DataGrid { async getDataGridTableData(): Promise { @@ -103,6 +108,137 @@ export function DataGridProvider({ getService }: FtrProviderContext) { [data-test-subj="dataGridRowCell"]:nth-of-type(${columnIndex})` ); } + public async getFields() { + const rows = await find.allByCssSelector('.euiDataGridRow'); + + const result = []; + for (const row of rows) { + const cells = await row.findAllByClassName('euiDataGridRowCell__truncate'); + const cellsText = []; + let cellIdx = 0; + for (const cell of cells) { + if (cellIdx > 0) { + cellsText.push(await cell.getVisibleText()); + } + cellIdx++; + } + result.push(cellsText); + } + return result; + } + + public async getTable(selector: string = 'docTable') { + return await testSubjects.find(selector); + } + + public async getBodyRows(): Promise { + const table = await this.getTable(); + return await table.findAllByTestSubject('dataGridRow'); + } + + public async getDocTableRows() { + const table = await this.getTable(); + return await table.findAllByTestSubject('dataGridRow'); + } + + public async getAnchorRow(): Promise { + const table = await this.getTable(); + return await table.findByTestSubject('~docTableAnchorRow'); + } + + public async getRow(options: SelectOptions): Promise { + return options.isAnchorRow + ? await this.getAnchorRow() + : (await this.getBodyRows())[options.rowIndex]; + } + + public async clickRowToggle( + options: SelectOptions = { isAnchorRow: false, rowIndex: 0 } + ): Promise { + const row = await this.getRow(options); + const toggle = await row.findByTestSubject('~docTableExpandToggleColumn'); + await toggle.click(); + } + + public async getDetailsRows(): Promise { + return await testSubjects.findAll('docTableDetailsFlyout'); + } + + public async closeFlyout() { + await testSubjects.click('euiFlyoutCloseButton'); + } + + public async getHeaderFields(): Promise { + const result = await find.allByCssSelector('.euiDataGridHeaderCell__content'); + const textArr = []; + let idx = 0; + for (const cell of result) { + if (idx > 0) { + textArr.push(await cell.getVisibleText()); + } + idx++; + } + return Promise.resolve(textArr); + } + + public async getRowActions( + options: SelectOptions = { isAnchorRow: false, rowIndex: 0 } + ): Promise { + const detailsRow = (await this.getDetailsRows())[options.rowIndex]; + return await detailsRow.findAllByTestSubject('~docTableRowAction'); + } + + public async clickDocSortAsc() { + await find.clickByCssSelector('.euiDataGridHeaderCell__button'); + await find.clickByButtonText('Sort New-Old'); + } + + public async clickDocSortDesc() { + await find.clickByCssSelector('.euiDataGridHeaderCell__button'); + await find.clickByButtonText('Sort Old-New'); + } + public async getDetailsRow(): Promise { + const detailRows = await this.getDetailsRows(); + return detailRows[0]; + } + public async addInclusiveFilter( + detailsRow: WebElementWrapper, + fieldName: string + ): Promise { + const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName); + const addInclusiveFilterButton = await this.getAddInclusiveFilterButton(tableDocViewRow); + await addInclusiveFilterButton.click(); + await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + } + + public async getAddInclusiveFilterButton( + tableDocViewRow: WebElementWrapper + ): Promise { + return await tableDocViewRow.findByTestSubject(`~addInclusiveFilterButton`); + } + + public async getTableDocViewRow( + detailsRow: WebElementWrapper, + fieldName: string + ): Promise { + return await detailsRow.findByTestSubject(`~tableDocViewRow-${fieldName}`); + } + + public async getRemoveInclusiveFilterButton( + tableDocViewRow: WebElementWrapper + ): Promise { + return await tableDocViewRow.findByTestSubject(`~removeInclusiveFilterButton`); + } + + public async removeInclusiveFilter( + detailsRow: WebElementWrapper, + fieldName: string + ): Promise { + const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName); + const addInclusiveFilterButton = await this.getRemoveInclusiveFilterButton(tableDocViewRow); + await addInclusiveFilterButton.click(); + await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + } } return new DataGrid(); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx index baa9cb7dd74f9..5d73cbc4cd3c8 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx @@ -23,7 +23,7 @@ describe('KeyUXMetrics', () => { > = [ { defaultMessage: 'Action' } ), render: (jobId: string) => ( - + {i18n.translate( 'xpack.apm.settings.anomalyDetection.jobList.mlJobLinkText', { defaultMessage: 'View job in ML', } )} - + ), }, ]; diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx index e68f8a9809bf5..eebd03772f238 100644 --- a/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx @@ -44,7 +44,7 @@ const traceListColumns: Array> = [ _: string, { serviceName, transactionName, transactionType }: TraceGroup ) => ( - + ; + +export const items: ServiceListAPIResponse['items'] = [ + { + serviceName: 'opbeans-node', + transactionType: 'request', + agentName: 'nodejs', + transactionsPerMinute: { value: 0, timeseries: [] }, + transactionErrorRate: { value: 46.06666666666667, timeseries: [] }, + avgResponseTime: { value: null, timeseries: [] }, + environments: ['test'], + }, + { + serviceName: 'opbeans-python', + transactionType: 'page-load', + agentName: 'python', + transactionsPerMinute: { value: 86.93333333333334, timeseries: [] }, + transactionErrorRate: { value: 12.6, timeseries: [] }, + avgResponseTime: { value: 91535.42944785276, timeseries: [] }, + environments: [], + }, +]; diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx index 157d3ecc738a1..27a2cf6418ece 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx @@ -6,10 +6,16 @@ import { EuiFlexItem, EuiFlexGroup, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React from 'react'; +import React, { useMemo } from 'react'; import styled from 'styled-components'; import { ValuesType } from 'utility-types'; import { orderBy } from 'lodash'; +import { EuiIcon } from '@elastic/eui'; +import { EuiText } from '@elastic/eui'; +import { + TRANSACTION_PAGE_LOAD, + TRANSACTION_REQUEST, +} from '../../../../../common/transaction_types'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { ServiceHealthStatus } from '../../../../../common/service_health_status'; import { @@ -55,126 +61,6 @@ const ToolTipWrapper = styled.span` } `; -export const SERVICE_COLUMNS: Array> = [ - { - field: 'healthStatus', - name: i18n.translate('xpack.apm.servicesTable.healthColumnLabel', { - defaultMessage: 'Health', - }), - width: px(unit * 6), - sortable: true, - render: (_, { healthStatus }) => { - return ( - - ); - }, - }, - { - field: 'serviceName', - name: i18n.translate('xpack.apm.servicesTable.nameColumnLabel', { - defaultMessage: 'Name', - }), - width: '40%', - sortable: true, - render: (_, { serviceName, agentName }) => ( - - - - {agentName && ( - - - - )} - - - {formatString(serviceName)} - - - - - - ), - }, - { - field: 'environments', - name: i18n.translate('xpack.apm.servicesTable.environmentColumnLabel', { - defaultMessage: 'Environment', - }), - width: px(unit * 10), - sortable: true, - render: (_, { environments }) => ( - - ), - }, - { - field: 'avgResponseTime', - name: i18n.translate('xpack.apm.servicesTable.avgResponseTimeColumnLabel', { - defaultMessage: 'Avg. response time', - }), - sortable: true, - dataType: 'number', - render: (_, { avgResponseTime }) => ( - - ), - align: 'left', - width: px(unit * 10), - }, - { - field: 'transactionsPerMinute', - name: i18n.translate( - 'xpack.apm.servicesTable.transactionsPerMinuteColumnLabel', - { - defaultMessage: 'Trans. per minute', - } - ), - sortable: true, - dataType: 'number', - render: (_, { transactionsPerMinute }) => ( - - ), - align: 'left', - width: px(unit * 10), - }, - { - field: 'transactionErrorRate', - name: i18n.translate('xpack.apm.servicesTable.transactionErrorRate', { - defaultMessage: 'Error rate %', - }), - sortable: true, - dataType: 'number', - render: (_, { transactionErrorRate }) => { - const value = transactionErrorRate?.value; - - const valueLabel = asPercent(value, 1); - - return ( - - ); - }, - align: 'left', - width: px(unit * 10), - }, -]; - const SERVICE_HEALTH_STATUS_ORDER = [ ServiceHealthStatus.unknown, ServiceHealthStatus.healthy, @@ -182,59 +68,244 @@ const SERVICE_HEALTH_STATUS_ORDER = [ ServiceHealthStatus.critical, ]; +export function getServiceColumns({ + showTransactionTypeColumn, +}: { + showTransactionTypeColumn: boolean; +}): Array> { + return [ + { + field: 'healthStatus', + name: i18n.translate('xpack.apm.servicesTable.healthColumnLabel', { + defaultMessage: 'Health', + }), + width: px(unit * 6), + sortable: true, + render: (_, { healthStatus }) => { + return ( + + ); + }, + }, + { + field: 'serviceName', + name: i18n.translate('xpack.apm.servicesTable.nameColumnLabel', { + defaultMessage: 'Name', + }), + width: '40%', + sortable: true, + render: (_, { serviceName, agentName }) => ( + + + + {agentName && ( + + + + )} + + + {formatString(serviceName)} + + + + + + ), + }, + { + field: 'environments', + name: i18n.translate('xpack.apm.servicesTable.environmentColumnLabel', { + defaultMessage: 'Environment', + }), + width: px(unit * 10), + sortable: true, + render: (_, { environments }) => ( + + ), + }, + ...(showTransactionTypeColumn + ? [ + { + field: 'transactionType', + name: i18n.translate( + 'xpack.apm.servicesTable.transactionColumnLabel', + { + defaultMessage: 'Transaction type', + } + ), + width: px(unit * 10), + sortable: true, + }, + ] + : []), + { + field: 'avgResponseTime', + name: i18n.translate( + 'xpack.apm.servicesTable.avgResponseTimeColumnLabel', + { + defaultMessage: 'Avg. response time', + } + ), + sortable: true, + dataType: 'number', + render: (_, { avgResponseTime }) => ( + + ), + align: 'left', + width: px(unit * 10), + }, + { + field: 'transactionsPerMinute', + name: i18n.translate( + 'xpack.apm.servicesTable.transactionsPerMinuteColumnLabel', + { + defaultMessage: 'Trans. per minute', + } + ), + sortable: true, + dataType: 'number', + render: (_, { transactionsPerMinute }) => ( + + ), + align: 'left', + width: px(unit * 10), + }, + { + field: 'transactionErrorRate', + name: i18n.translate('xpack.apm.servicesTable.transactionErrorRate', { + defaultMessage: 'Error rate %', + }), + sortable: true, + dataType: 'number', + render: (_, { transactionErrorRate }) => { + const value = transactionErrorRate?.value; + + const valueLabel = asPercent(value, 1); + + return ( + + ); + }, + align: 'left', + width: px(unit * 10), + }, + ]; +} + export function ServiceList({ items, noItemsMessage }: Props) { const displayHealthStatus = items.some((item) => 'healthStatus' in item); + const showTransactionTypeColumn = items.some( + ({ transactionType }) => + transactionType !== TRANSACTION_REQUEST && + transactionType !== TRANSACTION_PAGE_LOAD + ); + + const serviceColumns = useMemo( + () => getServiceColumns({ showTransactionTypeColumn }), + [showTransactionTypeColumn] + ); + const columns = displayHealthStatus - ? SERVICE_COLUMNS - : SERVICE_COLUMNS.filter((column) => column.field !== 'healthStatus'); + ? serviceColumns + : serviceColumns.filter((column) => column.field !== 'healthStatus'); const initialSortField = displayHealthStatus ? 'healthStatus' : 'transactionsPerMinute'; return ( - { - // For healthStatus, sort items by healthStatus first, then by TPM - return sortField === 'healthStatus' - ? orderBy( - itemsToSort, - [ - (item) => { - return item.healthStatus - ? SERVICE_HEALTH_STATUS_ORDER.indexOf(item.healthStatus) - : -1; - }, - (item) => item.transactionsPerMinute?.value ?? 0, - ], - [sortDirection, sortDirection] - ) - : orderBy( - itemsToSort, - (item) => { - switch (sortField) { - // Use `?? -1` here so `undefined` will appear after/before `0`. - // In the table this will make the "N/A" items always at the - // bottom/top. - case 'avgResponseTime': - return item.avgResponseTime?.value ?? -1; - case 'transactionsPerMinute': - return item.transactionsPerMinute?.value ?? -1; - case 'transactionErrorRate': - return item.transactionErrorRate?.value ?? -1; - default: - return item[sortField as keyof typeof item]; + + + + + + )} + > + + + + + + {i18n.translate( + 'xpack.apm.servicesTable.metricsExplanationLabel', + { defaultMessage: 'What are these metrics?' } + )} + + + + + + { + // For healthStatus, sort items by healthStatus first, then by TPM + return sortField === 'healthStatus' + ? orderBy( + itemsToSort, + [ + (item) => { + return item.healthStatus + ? SERVICE_HEALTH_STATUS_ORDER.indexOf(item.healthStatus) + : -1; + }, + (item) => item.transactionsPerMinute?.value ?? 0, + ], + [sortDirection, sortDirection] + ) + : orderBy( + itemsToSort, + (item) => { + switch (sortField) { + // Use `?? -1` here so `undefined` will appear after/before `0`. + // In the table this will make the "N/A" items always at the + // bottom/top. + case 'avgResponseTime': + return item.avgResponseTime?.value ?? -1; + case 'transactionsPerMinute': + return item.transactionsPerMinute?.value ?? -1; + case 'transactionErrorRate': + return item.transactionErrorRate?.value ?? -1; + default: + return item[sortField as keyof typeof item]; + } + }, + sortDirection + ); + }} + /> + + ); } diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/service_list.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/service_list.test.tsx index 1c6fa9fe0447e..45a4afeb53235 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/service_list.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/service_list.test.tsx @@ -9,11 +9,8 @@ import { MemoryRouter } from 'react-router-dom'; import { ServiceHealthStatus } from '../../../../../common/service_health_status'; import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { mockMoment, renderWithTheme } from '../../../../utils/testHelpers'; -import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -import { ServiceList, SERVICE_COLUMNS } from './'; -import props from './__fixtures__/props.json'; - -type ServiceListAPIResponse = APIReturnType<'GET /api/apm/services'>; +import { getServiceColumns, ServiceList } from './'; +import { items } from './__fixtures__/service_api_mock_data'; function Wrapper({ children }: { children?: ReactNode }) { return ( @@ -36,10 +33,7 @@ describe('ServiceList', () => { it('renders with data', () => { expect(() => - renderWithTheme( - , - { wrapper: Wrapper } - ) + renderWithTheme(, { wrapper: Wrapper }) ).not.toThrowError(); }); @@ -61,9 +55,9 @@ describe('ServiceList', () => { }, environments: ['test'], }; - const renderedColumns = SERVICE_COLUMNS.map((c) => - c.render!(service[c.field!], service) - ); + const renderedColumns = getServiceColumns({ + showTransactionTypeColumn: false, + }).map((c) => c.render!(service[c.field!], service)); expect(renderedColumns[0]).toMatchInlineSnapshot(` { describe('without ML data', () => { it('does not render the health column', () => { - const { queryByText } = renderWithTheme( - , - { - wrapper: Wrapper, - } - ); + const { queryByText } = renderWithTheme(, { + wrapper: Wrapper, + }); const healthHeading = queryByText('Health'); expect(healthHeading).toBeNull(); }); it('sorts by transactions per minute', async () => { - const { findByTitle } = renderWithTheme( - , - { - wrapper: Wrapper, - } - ); + const { findByTitle } = renderWithTheme(, { + wrapper: Wrapper, + }); expect( await findByTitle('Trans. per minute; Sorted in descending order') @@ -103,12 +91,10 @@ describe('ServiceList', () => { it('renders the health column', async () => { const { findByTitle } = renderWithTheme( ({ - ...item, - healthStatus: ServiceHealthStatus.warning, - }) - )} + items={items.map((item) => ({ + ...item, + healthStatus: ServiceHealthStatus.warning, + }))} />, { wrapper: Wrapper } ); diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLExplorerLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLExplorerLink.test.tsx new file mode 100644 index 0000000000000..3f02ed082f564 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLExplorerLink.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Location } from 'history'; +import React from 'react'; +import { getRenderedHref } from '../../../../utils/testHelpers'; +import { MLExplorerLink } from './MLExplorerLink'; + +describe('MLExplorerLink', () => { + it('should produce the correct URL with jobId', async () => { + const href = await getRenderedHref( + () => ( + + ), + { + search: + '?rangeFrom=now/w&rangeTo=now-4h&refreshPaused=true&refreshInterval=0', + } as Location + ); + + expect(href).toMatchInlineSnapshot( + `"/app/ml/explorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:!t,value:0),time:(from:now%2Fw,to:now-4h))&_a=(explorer:(mlExplorerFilter:(),mlExplorerSwimlane:()))"` + ); + }); + + it('correctly encodes time range values', async () => { + const href = await getRenderedHref( + () => ( + + ), + { + search: + '?rangeFrom=2020-07-29T17:27:29.000Z&rangeTo=2020-07-29T18:45:00.000Z&refreshInterval=10000&refreshPaused=true', + } as Location + ); + + expect(href).toMatchInlineSnapshot( + `"/app/ml/explorer?_g=(ml:(jobIds:!(apm-production-485b-high_mean_transaction_duration)),refreshInterval:(pause:!t,value:10000),time:(from:'2020-07-29T17:27:29.000Z',to:'2020-07-29T18:45:00.000Z'))&_a=(explorer:(mlExplorerFilter:(),mlExplorerSwimlane:()))"` + ); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLExplorerLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLExplorerLink.tsx new file mode 100644 index 0000000000000..ca9eb063bd090 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLExplorerLink.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { ReactNode } from 'react'; +import { EuiLink } from '@elastic/eui'; +import { UI_SETTINGS } from '../../../../../../../../src/plugins/data/common'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { useMlHref, ML_PAGES } from '../../../../../../ml/public'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { TimePickerRefreshInterval } from '../../DatePicker/typings'; + +interface Props { + children?: ReactNode; + jobId: string; + external?: boolean; +} + +export function MLExplorerLink({ jobId, external, children }: Props) { + const href = useExplorerHref({ jobId }); + + return ( + + ); +} + +export function useExplorerHref({ jobId }: { jobId: string }) { + const { + core, + plugins: { ml }, + } = useApmPluginContext(); + const { urlParams } = useUrlParams(); + + const timePickerRefreshIntervalDefaults = core.uiSettings.get( + UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS + ); + + const { + // hardcoding a custom default of 1 hour since the default kibana timerange of 15 minutes is shorter than the ML interval + rangeFrom = 'now-1h', + rangeTo = 'now', + refreshInterval = timePickerRefreshIntervalDefaults.value, + refreshPaused = timePickerRefreshIntervalDefaults.pause, + } = urlParams; + + const href = useMlHref(ml, core.http.basePath.get(), { + page: ML_PAGES.ANOMALY_EXPLORER, + pageState: { + jobIds: [jobId], + timeRange: { from: rangeFrom, to: rangeTo }, + refreshInterval: { pause: refreshPaused, value: refreshInterval }, + }, + }); + + return href; +} diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx index d125af70268cb..33dcbf02ccda7 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx @@ -10,6 +10,7 @@ import { isEmpty } from 'lodash'; import React from 'react'; import { useParams } from 'react-router-dom'; import styled from 'styled-components'; +import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { MLSingleMetricLink } from '../../Links/MachineLearningLinks/MLSingleMetricLink'; @@ -33,12 +34,13 @@ const ShiftedEuiText = styled(EuiText)` export function MLHeader({ hasValidMlLicense, mlJobId }: Props) { const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams } = useUrlParams(); + const { transactionType } = useApmServiceContext(); if (!hasValidMlLicense || !mlJobId) { return null; } - const { kuery, transactionType } = urlParams; + const { kuery } = urlParams; const hasKuery = !isEmpty(kuery); const icon = hasKuery ? ( diff --git a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts index 536be56e152a3..876fc6b822213 100644 --- a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts @@ -18,7 +18,10 @@ export function getOutcomeAggregation({ searchAggregatedTransactions: boolean; }) { return { - terms: { field: EVENT_OUTCOME }, + terms: { + field: EVENT_OUTCOME, + include: [EventOutcome.failure, EventOutcome.success], + }, aggs: { // simply using the doc count to get the number of requests is not possible for transaction metrics (histograms) // to work around this we get the number of transactions by counting the number of latency values diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts index 76a718bbb2a02..68bdc4b3d0ae8 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts @@ -132,7 +132,9 @@ export async function getWebCoreVitals({ return { coreVitalPages: coreVitalPages?.doc_count ?? 0, - cls: cls?.values[pkey]?.toFixed(3) || null, + /* Because cls is required in the type UXMetrics, and defined as number | null, + * we need to default to null in the case where cls is undefined in order to satisfy the UXMetrics type */ + cls: cls?.values[pkey] ?? null, fid: fid?.values[pkey], lcp: lcp?.values[pkey], tbt: tbt?.values[pkey] ?? 0, diff --git a/x-pack/plugins/apm/server/lib/service_nodes/index.ts b/x-pack/plugins/apm/server/lib/service_nodes/index.ts index d5e29532e3d7b..ca58a1b0e7126 100644 --- a/x-pack/plugins/apm/server/lib/service_nodes/index.ts +++ b/x-pack/plugins/apm/server/lib/service_nodes/index.ts @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { getServiceNodesProjection } from '../../projections/service_nodes'; -import { mergeProjection } from '../../projections/util/merge_projection'; -import { SERVICE_NODE_NAME_MISSING } from '../../../common/service_nodes'; import { - METRIC_PROCESS_CPU_PERCENT, - METRIC_JAVA_THREAD_COUNT, METRIC_JAVA_HEAP_MEMORY_USED, METRIC_JAVA_NON_HEAP_MEMORY_USED, + METRIC_JAVA_THREAD_COUNT, + METRIC_PROCESS_CPU_PERCENT, } from '../../../common/elasticsearch_fieldnames'; +import { SERVICE_NODE_NAME_MISSING } from '../../../common/service_nodes'; +import { getServiceNodesProjection } from '../../projections/service_nodes'; +import { mergeProjection } from '../../projections/util/merge_projection'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; const getServiceNodes = async ({ setup, @@ -68,15 +68,21 @@ const getServiceNodes = async ({ return []; } - return response.aggregations.nodes.buckets.map((bucket) => { - return { + return response.aggregations.nodes.buckets + .map((bucket) => ({ name: bucket.key as string, cpu: bucket.cpu.value, heapMemory: bucket.heapMemory.value, nonHeapMemory: bucket.nonHeapMemory.value, threadCount: bucket.threadCount.value, - }; - }); + })) + .filter( + (item) => + item.cpu !== null || + item.heapMemory !== null || + item.nonHeapMemory !== null || + item.threadCount != null + ); }; export { getServiceNodes }; diff --git a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index a6818f96c728e..21402e4c8dac0 100644 --- a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -100,196 +100,27 @@ Array [ "aggs": Object { "services": Object { "aggs": Object { - "average": Object { - "avg": Object { - "field": "transaction.duration.us", - }, - }, - "timeseries": Object { + "transactionType": Object { "aggs": Object { - "average": Object { - "avg": Object { - "field": "transaction.duration.us", + "agentName": Object { + "top_hits": Object { + "docvalue_fields": Array [ + "agent.name", + ], + "size": 1, }, }, - }, - "date_histogram": Object { - "extended_bounds": Object { - "max": 1528977600000, - "min": 1528113600000, - }, - "field": "@timestamp", - "fixed_interval": "43200s", - "min_doc_count": 0, - }, - }, - }, - "terms": Object { - "field": "service.name", - "size": 500, - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, - }, - }, - }, - Object { - "term": Object { - "service.environment": "test", - }, - }, - ], - }, - }, - "size": 0, - }, - }, - Object { - "apm": Object { - "events": Array [ - "transaction", - "metric", - "error", - ], - }, - "body": Object { - "aggs": Object { - "services": Object { - "aggs": Object { - "agent_name": Object { - "top_hits": Object { - "_source": Array [ - "agent.name", - ], - "size": 1, - }, - }, - }, - "terms": Object { - "field": "service.name", - "size": 500, - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, - }, - }, - }, - Object { - "term": Object { - "service.environment": "test", - }, - }, - ], - }, - }, - "size": 0, - }, - }, - Object { - "apm": Object { - "events": Array [ - "transaction", - ], - }, - "body": Object { - "aggs": Object { - "services": Object { - "aggs": Object { - "count": Object { - "value_count": Object { - "field": "transaction.duration.us", - }, - }, - "timeseries": Object { - "aggs": Object { - "count": Object { - "value_count": Object { + "avg_duration": Object { + "avg": Object { "field": "transaction.duration.us", }, }, - }, - "date_histogram": Object { - "extended_bounds": Object { - "max": 1528977600000, - "min": 1528113600000, - }, - "field": "@timestamp", - "fixed_interval": "43200s", - "min_doc_count": 0, - }, - }, - }, - "terms": Object { - "field": "service.name", - "size": 500, - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, - }, - }, - }, - Object { - "term": Object { - "service.environment": "test", - }, - }, - ], - }, - }, - "size": 0, - }, - }, - Object { - "apm": Object { - "events": Array [ - "transaction", - ], - }, - "body": Object { - "aggs": Object { - "services": Object { - "aggs": Object { - "outcomes": Object { - "aggs": Object { - "count": Object { - "value_count": Object { - "field": "transaction.duration.us", + "environments": Object { + "terms": Object { + "field": "service.environment", + "missing": "", }, }, - }, - "terms": Object { - "field": "event.outcome", - }, - }, - "timeseries": Object { - "aggs": Object { "outcomes": Object { "aggs": Object { "count": Object { @@ -300,73 +131,62 @@ Array [ }, "terms": Object { "field": "event.outcome", + "include": Array [ + "failure", + "success", + ], }, }, - }, - "date_histogram": Object { - "extended_bounds": Object { - "max": 1528977600000, - "min": 1528113600000, + "real_document_count": Object { + "value_count": Object { + "field": "transaction.duration.us", + }, }, - "field": "@timestamp", - "fixed_interval": "43200s", - "min_doc_count": 0, - }, - }, - }, - "terms": Object { - "field": "service.name", - "size": 500, - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, + "timeseries": Object { + "aggs": Object { + "avg_duration": Object { + "avg": Object { + "field": "transaction.duration.us", + }, + }, + "outcomes": Object { + "aggs": Object { + "count": Object { + "value_count": Object { + "field": "transaction.duration.us", + }, + }, + }, + "terms": Object { + "field": "event.outcome", + "include": Array [ + "failure", + "success", + ], + }, + }, + "real_document_count": Object { + "value_count": Object { + "field": "transaction.duration.us", + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "43200s", + "min_doc_count": 0, + }, }, }, - }, - Object { - "term": Object { - "service.environment": "test", - }, - }, - Object { - "terms": Object { - "event.outcome": Array [ - "failure", - "success", - ], - }, - }, - ], - }, - }, - "size": 0, - }, - }, - Object { - "apm": Object { - "events": Array [ - "transaction", - "metric", - "error", - ], - }, - "body": Object { - "aggs": Object { - "services": Object { - "aggs": Object { - "environments": Object { "terms": Object { - "field": "service.environment", - "size": 100, + "field": "transaction.type", + "order": Object { + "real_document_count": "desc", + }, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_health_statuses.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_health_statuses.ts new file mode 100644 index 0000000000000..206827a744113 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_health_statuses.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getSeverity } from '../../../../common/anomaly_detection'; +import { getServiceHealthStatus } from '../../../../common/service_health_status'; +import { + getMLJobIds, + getServiceAnomalies, +} from '../../service_map/get_service_anomalies'; +import { + ServicesItemsProjection, + ServicesItemsSetup, +} from './get_services_items'; + +interface AggregationParams { + setup: ServicesItemsSetup; + projection: ServicesItemsProjection; + searchAggregatedTransactions: boolean; +} + +export const getHealthStatuses = async ( + { setup }: AggregationParams, + mlAnomaliesEnvironment?: string +) => { + if (!setup.ml) { + return []; + } + + const jobIds = await getMLJobIds( + setup.ml.anomalyDetectors, + mlAnomaliesEnvironment + ); + if (!jobIds.length) { + return []; + } + + const anomalies = await getServiceAnomalies({ + setup, + environment: mlAnomaliesEnvironment, + }); + + return Object.keys(anomalies.serviceAnomalies).map((serviceName) => { + const stats = anomalies.serviceAnomalies[serviceName]; + + const severity = getSeverity(stats.anomalyScore); + const healthStatus = getServiceHealthStatus({ severity }); + + return { + serviceName, + healthStatus, + }; + }); +}; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts new file mode 100644 index 0000000000000..0ee7080dc0834 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.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 { + AGENT_NAME, + SERVICE_ENVIRONMENT, + SERVICE_NAME, + TRANSACTION_TYPE, +} from '../../../../common/elasticsearch_fieldnames'; +import { + TRANSACTION_PAGE_LOAD, + TRANSACTION_REQUEST, +} from '../../../../common/transaction_types'; +import { rangeFilter } from '../../../../common/utils/range_filter'; +import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; +import { + getDocumentTypeFilterForAggregatedTransactions, + getProcessorEventForAggregatedTransactions, + getTransactionDurationFieldForAggregatedTransactions, +} from '../../helpers/aggregated_transactions'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { + calculateTransactionErrorPercentage, + getOutcomeAggregation, +} from '../../helpers/transaction_error_rate'; +import { ServicesItemsSetup } from './get_services_items'; + +interface AggregationParams { + setup: ServicesItemsSetup; + searchAggregatedTransactions: boolean; +} + +const MAX_NUMBER_OF_SERVICES = 500; + +function calculateAvgDuration({ + value, + deltaAsMinutes, +}: { + value: number; + deltaAsMinutes: number; +}) { + return value / deltaAsMinutes; +} + +export async function getServiceTransactionStats({ + setup, + searchAggregatedTransactions, +}: AggregationParams) { + const { apmEventClient, start, end, esFilter } = setup; + + const outcomes = getOutcomeAggregation({ searchAggregatedTransactions }); + + const metrics = { + real_document_count: { + value_count: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + avg_duration: { + avg: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + outcomes, + }; + + const response = await apmEventClient.search({ + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { range: rangeFilter(start, end) }, + ...esFilter, + ...getDocumentTypeFilterForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + }, + aggs: { + services: { + terms: { + field: SERVICE_NAME, + size: MAX_NUMBER_OF_SERVICES, + }, + aggs: { + transactionType: { + terms: { + field: TRANSACTION_TYPE, + order: { real_document_count: 'desc' }, + }, + aggs: { + ...metrics, + environments: { + terms: { + field: SERVICE_ENVIRONMENT, + missing: '', + }, + }, + agentName: { + top_hits: { + docvalue_fields: [AGENT_NAME] as const, + size: 1, + }, + }, + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: getBucketSize({ + start, + end, + numBuckets: 20, + }).intervalString, + min_doc_count: 0, + extended_bounds: { min: start, max: end }, + }, + aggs: metrics, + }, + }, + }, + }, + }, + }, + }, + }); + + const deltaAsMinutes = (setup.end - setup.start) / 1000 / 60; + + return ( + response.aggregations?.services.buckets.map((bucket) => { + const topTransactionTypeBucket = + bucket.transactionType.buckets.find( + ({ key }) => + key === TRANSACTION_REQUEST || key === TRANSACTION_PAGE_LOAD + ) ?? bucket.transactionType.buckets[0]; + + return { + serviceName: bucket.key as string, + transactionType: topTransactionTypeBucket.key as string, + environments: topTransactionTypeBucket.environments.buckets + .map((environmentBucket) => environmentBucket.key as string) + .filter(Boolean), + agentName: topTransactionTypeBucket.agentName.hits.hits[0].fields[ + 'agent.name' + ]?.[0] as AgentName, + avgResponseTime: { + value: topTransactionTypeBucket.avg_duration.value, + timeseries: topTransactionTypeBucket.timeseries.buckets.map( + (dateBucket) => ({ + x: dateBucket.key, + y: dateBucket.avg_duration.value, + }) + ), + }, + transactionErrorRate: { + value: calculateTransactionErrorPercentage( + topTransactionTypeBucket.outcomes + ), + timeseries: topTransactionTypeBucket.timeseries.buckets.map( + (dateBucket) => ({ + x: dateBucket.key, + y: calculateTransactionErrorPercentage(dateBucket.outcomes), + }) + ), + }, + transactionsPerMinute: { + value: calculateAvgDuration({ + value: topTransactionTypeBucket.real_document_count.value, + deltaAsMinutes, + }), + timeseries: topTransactionTypeBucket.timeseries.buckets.map( + (dateBucket) => ({ + x: dateBucket.key, + y: calculateAvgDuration({ + value: dateBucket.real_document_count.value, + deltaAsMinutes, + }), + }) + ), + }, + }; + }) ?? [] + ); +} diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index 11f3e44fce87c..359c677b00baf 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -7,14 +7,8 @@ import { Logger } from '@kbn/logging'; import { joinByKey } from '../../../../common/utils/join_by_key'; import { getServicesProjection } from '../../../projections/services'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { - getAgentNames, - getEnvironments, - getHealthStatuses, - getTransactionDurationAverages, - getTransactionErrorRates, - getTransactionRates, -} from './get_services_items_stats'; +import { getHealthStatuses } from './get_health_statuses'; +import { getServiceTransactionStats } from './get_service_transaction_stats'; export type ServicesItemsSetup = Setup & SetupTimeRange; export type ServicesItemsProjection = ReturnType; @@ -37,46 +31,23 @@ export async function getServicesItems({ searchAggregatedTransactions, }; - const [ - transactionDurationAverages, - agentNames, - transactionRates, - transactionErrorRates, - environments, - healthStatuses, - ] = await Promise.all([ - getTransactionDurationAverages(params), - getAgentNames(params), - getTransactionRates(params), - getTransactionErrorRates(params), - getEnvironments(params), + const [transactionStats, healthStatuses] = await Promise.all([ + getServiceTransactionStats(params), getHealthStatuses(params, setup.uiFilters.environment).catch((err) => { logger.error(err); return []; }), ]); - const apmServiceMetrics = joinByKey( - [ - ...transactionDurationAverages, - ...agentNames, - ...transactionRates, - ...transactionErrorRates, - ...environments, - ], - 'serviceName' - ); - - const apmServices = apmServiceMetrics.map(({ serviceName }) => serviceName); + const apmServices = transactionStats.map(({ serviceName }) => serviceName); // make sure to exclude health statuses from services // that are not found in APM data - const matchedHealthStatuses = healthStatuses.filter(({ serviceName }) => apmServices.includes(serviceName) ); - const allMetrics = [...apmServiceMetrics, ...matchedHealthStatuses]; + const allMetrics = [...transactionStats, ...matchedHealthStatuses]; return joinByKey(allMetrics, 'serviceName'); } diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts deleted file mode 100644 index c8ebaa13d9df9..0000000000000 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts +++ /dev/null @@ -1,413 +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 { getServiceHealthStatus } from '../../../../common/service_health_status'; -import { EventOutcome } from '../../../../common/event_outcome'; -import { getSeverity } from '../../../../common/anomaly_detection'; -import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; -import { - AGENT_NAME, - SERVICE_ENVIRONMENT, - EVENT_OUTCOME, -} from '../../../../common/elasticsearch_fieldnames'; -import { mergeProjection } from '../../../projections/util/merge_projection'; -import { - ServicesItemsSetup, - ServicesItemsProjection, -} from './get_services_items'; -import { - getDocumentTypeFilterForAggregatedTransactions, - getProcessorEventForAggregatedTransactions, - getTransactionDurationFieldForAggregatedTransactions, -} from '../../helpers/aggregated_transactions'; -import { getBucketSize } from '../../helpers/get_bucket_size'; -import { - getMLJobIds, - getServiceAnomalies, -} from '../../service_map/get_service_anomalies'; -import { - calculateTransactionErrorPercentage, - getOutcomeAggregation, - getTransactionErrorRateTimeSeries, -} from '../../helpers/transaction_error_rate'; - -function getDateHistogramOpts(start: number, end: number) { - return { - field: '@timestamp', - fixed_interval: getBucketSize({ start, end, numBuckets: 20 }) - .intervalString, - min_doc_count: 0, - extended_bounds: { min: start, max: end }, - }; -} - -const MAX_NUMBER_OF_SERVICES = 500; - -const getDeltaAsMinutes = (setup: ServicesItemsSetup) => - (setup.end - setup.start) / 1000 / 60; - -interface AggregationParams { - setup: ServicesItemsSetup; - projection: ServicesItemsProjection; - searchAggregatedTransactions: boolean; -} - -export const getTransactionDurationAverages = async ({ - setup, - projection, - searchAggregatedTransactions, -}: AggregationParams) => { - const { apmEventClient, start, end } = setup; - - const response = await apmEventClient.search( - mergeProjection(projection, { - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { - size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - ...getDocumentTypeFilterForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - }, - aggs: { - services: { - terms: { - ...projection.body.aggs.services.terms, - size: MAX_NUMBER_OF_SERVICES, - }, - aggs: { - average: { - avg: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, - }, - timeseries: { - date_histogram: getDateHistogramOpts(start, end), - aggs: { - average: { - avg: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, - }, - }, - }, - }, - }, - }, - }, - }) - ); - - const { aggregations } = response; - - if (!aggregations) { - return []; - } - - return aggregations.services.buckets.map((serviceBucket) => ({ - serviceName: serviceBucket.key as string, - avgResponseTime: { - value: serviceBucket.average.value, - timeseries: serviceBucket.timeseries.buckets.map((dateBucket) => ({ - x: dateBucket.key, - y: dateBucket.average.value, - })), - }, - })); -}; - -export const getAgentNames = async ({ - setup, - projection, -}: AggregationParams) => { - const { apmEventClient } = setup; - const response = await apmEventClient.search( - mergeProjection(projection, { - body: { - size: 0, - aggs: { - services: { - terms: { - ...projection.body.aggs.services.terms, - size: MAX_NUMBER_OF_SERVICES, - }, - aggs: { - agent_name: { - top_hits: { - _source: [AGENT_NAME], - size: 1, - }, - }, - }, - }, - }, - }, - }) - ); - - const { aggregations } = response; - - if (!aggregations) { - return []; - } - - return aggregations.services.buckets.map((serviceBucket) => ({ - serviceName: serviceBucket.key as string, - agentName: serviceBucket.agent_name.hits.hits[0]?._source.agent - .name as AgentName, - })); -}; - -export const getTransactionRates = async ({ - setup, - projection, - searchAggregatedTransactions, -}: AggregationParams) => { - const { apmEventClient, start, end } = setup; - const response = await apmEventClient.search( - mergeProjection(projection, { - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { - size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - ...getDocumentTypeFilterForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - }, - aggs: { - services: { - terms: { - ...projection.body.aggs.services.terms, - size: MAX_NUMBER_OF_SERVICES, - }, - aggs: { - count: { - value_count: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, - }, - timeseries: { - date_histogram: getDateHistogramOpts(start, end), - aggs: { - count: { - value_count: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, - }, - }, - }, - }, - }, - }, - }, - }) - ); - - const { aggregations } = response; - - if (!aggregations) { - return []; - } - - const deltaAsMinutes = getDeltaAsMinutes(setup); - - return aggregations.services.buckets.map((serviceBucket) => { - const transactionsPerMinute = serviceBucket.count.value / deltaAsMinutes; - return { - serviceName: serviceBucket.key as string, - transactionsPerMinute: { - value: transactionsPerMinute, - timeseries: serviceBucket.timeseries.buckets.map((dateBucket) => ({ - x: dateBucket.key, - y: dateBucket.count.value / deltaAsMinutes, - })), - }, - }; - }); -}; - -export const getTransactionErrorRates = async ({ - setup, - projection, - searchAggregatedTransactions, -}: AggregationParams) => { - const { apmEventClient, start, end } = setup; - - const outcomes = getOutcomeAggregation({ searchAggregatedTransactions }); - - const response = await apmEventClient.search( - mergeProjection(projection, { - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { - size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - { - terms: { - [EVENT_OUTCOME]: [EventOutcome.failure, EventOutcome.success], - }, - }, - ], - }, - }, - aggs: { - services: { - terms: { - ...projection.body.aggs.services.terms, - size: MAX_NUMBER_OF_SERVICES, - }, - aggs: { - outcomes, - timeseries: { - date_histogram: getDateHistogramOpts(start, end), - aggs: { - outcomes, - }, - }, - }, - }, - }, - }, - }) - ); - - const { aggregations } = response; - - if (!aggregations) { - return []; - } - - return aggregations.services.buckets.map((serviceBucket) => { - const transactionErrorRate = calculateTransactionErrorPercentage( - serviceBucket.outcomes - ); - return { - serviceName: serviceBucket.key as string, - transactionErrorRate: { - value: transactionErrorRate, - timeseries: getTransactionErrorRateTimeSeries( - serviceBucket.timeseries.buckets - ), - }, - }; - }); -}; - -export const getEnvironments = async ({ - setup, - projection, -}: AggregationParams) => { - const { apmEventClient, config } = setup; - const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments']; - const response = await apmEventClient.search( - mergeProjection(projection, { - body: { - size: 0, - aggs: { - services: { - terms: { - ...projection.body.aggs.services.terms, - size: MAX_NUMBER_OF_SERVICES, - }, - aggs: { - environments: { - terms: { - field: SERVICE_ENVIRONMENT, - size: maxServiceEnvironments, - }, - }, - }, - }, - }, - }, - }) - ); - - const { aggregations } = response; - - if (!aggregations) { - return []; - } - - return aggregations.services.buckets.map((serviceBucket) => ({ - serviceName: serviceBucket.key as string, - environments: serviceBucket.environments.buckets.map( - (envBucket) => envBucket.key as string - ), - })); -}; - -export const getHealthStatuses = async ( - { setup }: AggregationParams, - mlAnomaliesEnvironment?: string -) => { - if (!setup.ml) { - return []; - } - - const jobIds = await getMLJobIds( - setup.ml.anomalyDetectors, - mlAnomaliesEnvironment - ); - if (!jobIds.length) { - return []; - } - - const anomalies = await getServiceAnomalies({ - setup, - environment: mlAnomaliesEnvironment, - }); - - return Object.keys(anomalies.serviceAnomalies).map((serviceName) => { - const stats = anomalies.serviceAnomalies[serviceName]; - - const severity = getSeverity(stats.anomalyScore); - const healthStatus = getServiceHealthStatus({ severity }); - - return { - serviceName, - healthStatus, - }; - }); -}; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx index 54702f2654839..34f4bb39fbfa7 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx @@ -12,6 +12,7 @@ export const defaultHandlers: RendererHandlers = { getElementId: () => 'element-id', getFilter: () => 'filter', getRenderMode: () => 'display', + isSyncColorsEnabled: () => false, onComplete: (fn) => undefined, onEmbeddableDestroyed: action('onEmbeddableDestroyed'), onEmbeddableInputChange: action('onEmbeddableInputChange'), diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot index 1f7105b80de4c..d267ba07078fe 100644 --- a/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot @@ -178,11 +178,7 @@ exports[`Storyshots components/WorkpadTemplates default 1`] = ` > - } + title="; Sorted in ascending order" > Template name diff --git a/x-pack/plugins/canvas/public/lib/create_handlers.ts b/x-pack/plugins/canvas/public/lib/create_handlers.ts index 9bc4bd5e78fd0..4c9dbd92d3f21 100644 --- a/x-pack/plugins/canvas/public/lib/create_handlers.ts +++ b/x-pack/plugins/canvas/public/lib/create_handlers.ts @@ -26,6 +26,9 @@ export const createHandlers = (): RendererHandlers => ({ getRenderMode() { return 'display'; }, + isSyncColorsEnabled() { + return false; + }, onComplete(fn: () => void) { this.done = fn; }, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx index 4f5ded0a3ccc1..ec018f0faf5ff 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx @@ -204,7 +204,11 @@ describe('Credentials', () => { copy: expect.any(Function), toggleIsHidden: expect.any(Function), isHidden: expect.any(Boolean), - text: •••••••, + text: ( + + ••••••• + + ), }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx index 9240bade4975e..df85a9c3053a6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx @@ -40,6 +40,7 @@ export const CredentialsList: React.FC = () => { { name: 'Key', width: '36%', + className: 'eui-textBreakAll', render: (token: ApiToken) => { const { key } = token; if (!key) return null; @@ -60,6 +61,10 @@ export const CredentialsList: React.FC = () => { ); }, + mobileOptions: { + // @ts-ignore - EUI's type definitions need to be updated + width: '100%', + }, }, { name: 'Modes', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.tsx index fa2d124cbccdf..8ea2b6c284fc6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.tsx @@ -39,6 +39,7 @@ export const Key: React.FC = ({ copy, toggleIsHidden, isHidden, text }) = iconType={hideIcon} aria-label={hideIconLabel} aria-pressed={!isHidden} + style={{ marginRight: '0.25em' }} /> {text} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.tsx index 9ebe404659ca2..c33cda9f7e429 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.tsx @@ -23,6 +23,8 @@ import { EuiBadge, EuiCode, EuiCodeBlock, + EuiFlexGroup, + EuiFlexItem, } from '@elastic/eui'; import { getEnterpriseSearchUrl } from '../../../../shared/enterprise_search_url'; @@ -95,8 +97,14 @@ export const FlyoutBody: React.FC = () => { - POST - {documentsApiUrl} + + + POST + + + {documentsApiUrl} + + {dedent(` diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.scss index 8342061ee00c3..f69acbdaba150 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.scss @@ -1,17 +1,43 @@ .appSearchResult { - display: flex; + display: grid; + grid-template-columns: 1fr auto; + grid-template-rows: 1fr auto; + grid-template-areas: + 'content actions' + 'toggle actions'; + overflow: hidden; // Prevents child background-colors from clipping outside of panel border-radius &__content { + grid-area: content; width: 100%; padding: $euiSize; overflow: hidden; color: $euiTextColor; } - &__hiddenFieldsIndicator { + &__hiddenFieldsToggle { + grid-area: toggle; + display: flex; + justify-content: center; + padding: $euiSizeS; + border-top: $euiBorderThin; font-size: $euiFontSizeXS; - color: $euiColorDarkShade; - margin-top: $euiSizeS; + color: $euiColorPrimary; + + &:hover, + &:focus { + background-color: $euiPageBackgroundColor; + } + + .euiIcon { + margin-left: $euiSizeXS; + } + } + + &__actionButtons { + grid-area: actions; + display: flex; + flex-wrap: no-wrap; } &__actionButton { @@ -22,10 +48,27 @@ border-left: $euiBorderThin; &:hover, - &:focus, - &:active { + &:focus { background-color: $euiPageBackgroundColor; - cursor: pointer; } } } + +/** + * CSS for hover specific logic + * It's mildly horrific, so I pulled it out to its own section here + */ + +.appSearchResult--link { + &:hover, + &:focus { + @include euiSlightShadowHover; + } +} +.appSearchResult__content--link:hover { + cursor: pointer; + + & ~ .appSearchResult__actionButtons .appSearchResult__actionButton--link { + background-color: $euiPageBackgroundColor; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx index c4de24e78eae5..973fc6226910a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx @@ -49,6 +49,7 @@ describe('Result', () => { it('renders', () => { const wrapper = shallow(); expect(wrapper.find(EuiPanel).exists()).toBe(true); + expect(wrapper.find(EuiPanel).prop('title')).toEqual('Document 1'); }); it('should render a ResultField for each field except id and _meta', () => { @@ -76,16 +77,20 @@ describe('Result', () => { describe('document detail link', () => { it('will render a link if shouldLinkToDetailPage is true', () => { const wrapper = shallow(); - expect(wrapper.find(ReactRouterHelper).prop('to')).toEqual('/engines/my-engine/documents/1'); - expect(wrapper.find('article.appSearchResult__content').exists()).toBe(false); - expect(wrapper.find('a.appSearchResult__content').exists()).toBe(true); + wrapper.find(ReactRouterHelper).forEach((link) => { + expect(link.prop('to')).toEqual('/engines/my-engine/documents/1'); + }); + expect(wrapper.hasClass('appSearchResult--link')).toBe(true); + expect(wrapper.find('.appSearchResult__content--link').exists()).toBe(true); + expect(wrapper.find('.appSearchResult__actionButton--link').exists()).toBe(true); }); it('will not render a link if shouldLinkToDetailPage is not set', () => { const wrapper = shallow(); expect(wrapper.find(ReactRouterHelper).exists()).toBe(false); - expect(wrapper.find('article.appSearchResult__content').exists()).toBe(true); - expect(wrapper.find('a.appSearchResult__content').exists()).toBe(false); + expect(wrapper.hasClass('appSearchResult--link')).toBe(false); + expect(wrapper.find('.appSearchResult__content--link').exists()).toBe(false); + expect(wrapper.find('.appSearchResult__actionButton--link').exists()).toBe(false); }); }); @@ -140,18 +145,16 @@ describe('Result', () => { wrapper = shallow(); }); - it('renders a collapse button', () => { - expect(wrapper.find('[data-test-subj="CollapseResult"]').exists()).toBe(false); + it('renders a hidden fields toggle button', () => { + expect(wrapper.find('.appSearchResult__hiddenFieldsToggle').exists()).toBe(true); }); - it('does not render an expand button', () => { - expect(wrapper.find('[data-test-subj="ExpandResult"]').exists()).toBe(true); + it('renders a collapse icon', () => { + expect(wrapper.find('[data-test-subj="CollapseResult"]').exists()).toBe(false); }); - it('renders a hidden fields indicator', () => { - expect(wrapper.find('.appSearchResult__hiddenFieldsIndicator').text()).toEqual( - '1 more fields' - ); + it('does not render an expand icon', () => { + expect(wrapper.find('[data-test-subj="ExpandResult"]').exists()).toBe(true); }); it('shows no more than 5 fields', () => { @@ -164,20 +167,22 @@ describe('Result', () => { beforeAll(() => { wrapper = shallow(); - expect(wrapper.find('.appSearchResult__actionButton').exists()).toBe(true); - wrapper.find('.appSearchResult__actionButton').simulate('click'); + expect(wrapper.find('.appSearchResult__hiddenFieldsToggle').exists()).toBe(true); + wrapper.find('.appSearchResult__hiddenFieldsToggle').simulate('click'); }); - it('renders a collapse button', () => { - expect(wrapper.find('[data-test-subj="CollapseResult"]').exists()).toBe(true); + it('renders correct toggle text', () => { + expect(wrapper.find('.appSearchResult__hiddenFieldsToggle').text()).toEqual( + 'Hide additional fields' + ); }); - it('does not render an expand button', () => { - expect(wrapper.find('[data-test-subj="ExpandResult"]').exists()).toBe(false); + it('renders a collapse icon', () => { + expect(wrapper.find('[data-test-subj="CollapseResult"]').exists()).toBe(true); }); - it('does not render a hidden fields indicator', () => { - expect(wrapper.find('.appSearchResult__hiddenFieldsIndicator').exists()).toBe(false); + it('does not render an expand icon', () => { + expect(wrapper.find('[data-test-subj="ExpandResult"]').exists()).toBe(false); }); it('shows all fields', () => { @@ -190,23 +195,23 @@ describe('Result', () => { beforeAll(() => { wrapper = shallow(); - expect(wrapper.find('.appSearchResult__actionButton').exists()).toBe(true); - wrapper.find('.appSearchResult__actionButton').simulate('click'); - wrapper.find('.appSearchResult__actionButton').simulate('click'); + expect(wrapper.find('.appSearchResult__hiddenFieldsToggle').exists()).toBe(true); + wrapper.find('.appSearchResult__hiddenFieldsToggle').simulate('click'); + wrapper.find('.appSearchResult__hiddenFieldsToggle').simulate('click'); }); - it('renders a collapse button', () => { - expect(wrapper.find('[data-test-subj="CollapseResult"]').exists()).toBe(false); + it('renders correct toggle text', () => { + expect(wrapper.find('.appSearchResult__hiddenFieldsToggle').text()).toEqual( + 'Show 1 additional field' + ); }); - it('does not render an expand button', () => { - expect(wrapper.find('[data-test-subj="ExpandResult"]').exists()).toBe(true); + it('renders a collapse icon', () => { + expect(wrapper.find('[data-test-subj="CollapseResult"]').exists()).toBe(false); }); - it('renders a hidden fields indicator', () => { - expect(wrapper.find('.appSearchResult__hiddenFieldsIndicator').text()).toEqual( - '1 more fields' - ); + it('does not render an expand icon', () => { + expect(wrapper.find('[data-test-subj="ExpandResult"]').exists()).toBe(true); }); it('shows no more than 5 fields', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx index 76b06754d6ce6..f25eb2a4ba09e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx @@ -5,6 +5,7 @@ */ import React, { useState, useMemo } from 'react'; +import classNames from 'classnames'; import './result.scss'; @@ -49,23 +50,31 @@ export const Result: React.FC = ({ if (schemaForTypeHighlights) return schemaForTypeHighlights[fieldName]; }; + const documentLink = getDocumentDetailRoute(resultMeta.engine, resultMeta.id); const conditionallyLinkedArticle = (children: React.ReactNode) => { return shouldLinkToDetailPage ? ( - - {children} + +
+ {children} +
) : (
{children}
); }; + const classes = classNames('appSearchResult', { + 'appSearchResult--link': shouldLinkToDetailPage, + }); + return ( {conditionallyLinkedArticle( @@ -75,53 +84,57 @@ export const Result: React.FC = ({ showScore={!!showScore} isMetaEngine={isMetaEngine} /> -
- {resultFields - .slice(0, isOpen ? resultFields.length : RESULT_CUTOFF) - .map(([field, value]: [string, FieldValue]) => ( - - ))} -
- {numResults > RESULT_CUTOFF && !isOpen && ( -
- {i18n.translate('xpack.enterpriseSearch.appSearch.result.numberOfAdditionalFields', { - defaultMessage: '{numberOfAdditionalFields} more fields', - values: { - numberOfAdditionalFields: numResults - RESULT_CUTOFF, - }, - })} -
- )} + {resultFields + .slice(0, isOpen ? resultFields.length : RESULT_CUTOFF) + .map(([field, value]: [string, FieldValue]) => ( + + ))} )} {numResults > RESULT_CUTOFF && ( )} +
+ {shouldLinkToDetailPage && ( + + + + + + )} +
); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx index 69176b8a139e7..dae22a47035c4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx @@ -25,7 +25,9 @@ export const HiddenText: React.FC = ({ text, children }) => { defaultMessage: 'Hidden text', }); const hiddenText = isHidden ? ( - {text.replace(/./g, '•')} + + {text.replace(/./g, '•')} + ) : ( text ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/linked_agent_count.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/linked_agent_count.tsx new file mode 100644 index 0000000000000..bbe7f1254a140 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/components/linked_agent_count.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo } from 'react'; +import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui'; +import { useLink } from '../hooks'; +import { AGENT_SAVED_OBJECT_TYPE } from '../constants'; + +/** + * Displays the provided `count` number as a link to the Agents list if it is greater than zero + */ +export const LinkedAgentCount = memo< + Omit & { count: number; agentPolicyId: string } +>(({ count, agentPolicyId, ...otherEuiLinkProps }) => { + const { getHref } = useLink(); + return count > 0 ? ( + + {count} + + ) : ( + + {count} + + ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/index.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/index.ts index 1ec43f4df8c8e..ca76b65518ebe 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/index.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/index.ts @@ -8,7 +8,7 @@ export { AgentPolicyCopyProvider } from './agent_policy_copy_provider'; export { AgentPolicyDeleteProvider } from './agent_policy_delete_provider'; export { PackagePolicyDeleteProvider } from './package_policy_delete_provider'; export { AgentPolicyYamlFlyout } from './agent_policy_yaml_flyout'; -export { LinkedAgentCount } from './linked_agent_count'; +export { LinkedAgentCount } from '../../../components/linked_agent_count'; export { ConfirmDeployAgentPolicyModal } from './confirm_deploy_modal'; export { DangerEuiContextMenuItem } from './danger_eui_context_menu_item'; export { AgentPolicyActionMenu } from './actions_menu'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/linked_agent_count.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/linked_agent_count.tsx deleted file mode 100644 index c602f492f74c6..0000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/linked_agent_count.tsx +++ /dev/null @@ -1,35 +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, { memo } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiLink } from '@elastic/eui'; -import { useLink } from '../../../hooks'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; - -export const LinkedAgentCount = memo<{ count: number; agentPolicyId: string }>( - ({ count, agentPolicyId }) => { - const { getHref } = useLink(); - const displayValue = ( - - ); - return count > 0 ? ( - - {displayValue} - - ) : ( - displayValue - ); - } -); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx index 3d43725f2dc71..2e4c65955e0da 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx @@ -166,11 +166,27 @@ describe('when on integration detail', () => { it('should link to integration policy detail when an integration policy is clicked', async () => { await mockedApi.waitForApi(); - const firstPolicy = renderResult.getByTestId('integrationNameLink') as HTMLAnchorElement; + const firstPolicy = renderResult.getAllByTestId( + 'integrationNameLink' + )[0] as HTMLAnchorElement; expect(firstPolicy.href).toEqual( 'http://localhost/mock/app/fleet#/integrations/edit-integration/e8a37031-2907-44f6-89d2-98bd493f60dc' ); }); + + it('should NOT show link for agent count if it is zero', async () => { + await mockedApi.waitForApi(); + const firstRowAgentCount = renderResult.getAllByTestId('rowAgentCount')[0]; + expect(firstRowAgentCount.textContent).toEqual('0'); + expect(firstRowAgentCount.tagName).not.toEqual('A'); + }); + + it('should show link for agent count if greater than zero', async () => { + await mockedApi.waitForApi(); + const secondRowAgentCount = renderResult.getAllByTestId('rowAgentCount')[1]; + expect(secondRowAgentCount.textContent).toEqual('100'); + expect(secondRowAgentCount.tagName).toEqual('A'); + }); }); }); @@ -522,8 +538,87 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos updated_at: '2020-12-09T13:46:31.013Z', updated_by: 'elastic', }, + { + id: 'e3t37031-2907-44f6-89d2-5555555555', + version: 'WrrrMiwxXQ==', + name: 'nginx-2', + description: '', + namespace: 'default', + policy_id: '125c1b70-3976-11eb-ad1c-3baa423085y6', + enabled: true, + output_id: '', + inputs: [ + { + type: 'logfile', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'logs', dataset: 'nginx.access' }, + vars: { paths: { value: ['/var/log/nginx/access.log*'], type: 'text' } }, + id: 'logfile-nginx.access-e8a37031-2907-44f6-89d2-98bd493f60dc', + compiled_stream: { + paths: ['/var/log/nginx/access.log*'], + exclude_files: ['.gz$'], + processors: [{ add_locale: null }], + }, + }, + { + enabled: true, + data_stream: { type: 'logs', dataset: 'nginx.error' }, + vars: { paths: { value: ['/var/log/nginx/error.log*'], type: 'text' } }, + id: 'logfile-nginx.error-e8a37031-2907-44f6-89d2-98bd493f60dc', + compiled_stream: { + paths: ['/var/log/nginx/error.log*'], + exclude_files: ['.gz$'], + multiline: { + pattern: '^\\d{4}\\/\\d{2}\\/\\d{2} ', + negate: true, + match: 'after', + }, + processors: [{ add_locale: null }], + }, + }, + { + enabled: false, + data_stream: { type: 'logs', dataset: 'nginx.ingress_controller' }, + vars: { paths: { value: ['/var/log/nginx/ingress.log*'], type: 'text' } }, + id: 'logfile-nginx.ingress_controller-e8a37031-2907-44f6-89d2-98bd493f60dc', + }, + ], + }, + { + type: 'nginx/metrics', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'nginx.stubstatus' }, + vars: { + period: { value: '10s', type: 'text' }, + server_status_path: { value: '/nginx_status', type: 'text' }, + }, + id: 'nginx/metrics-nginx.stubstatus-e8a37031-2907-44f6-89d2-98bd493f60dc', + compiled_stream: { + metricsets: ['stubstatus'], + hosts: ['http://127.0.0.1:80'], + period: '10s', + server_status_path: '/nginx_status', + }, + }, + ], + vars: { hosts: { value: ['http://127.0.0.1:80'], type: 'text' } }, + }, + ], + package: { name: 'nginx', title: 'Nginx', version: '0.3.7' }, + revision: 3, + created_at: '2020-12-09T13:46:31.013Z', + created_by: 'elastic', + updated_at: '2020-12-09T13:46:31.013Z', + updated_by: 'elastic', + }, ], - total: 1, + total: 2, page: 1, perPage: 20, }; @@ -548,8 +643,22 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos updated_by: 'elastic', agents: 0, }, + { + id: '125c1b70-3976-11eb-ad1c-3baa423085y6', + name: 'EU Healthy agents', + namespace: 'default', + description: 'Protect EU from COVID', + status: 'active', + package_policies: ['e8a37031-2907-44f6-89d2-98bd493f60cd'], + is_default: false, + monitoring_enabled: ['logs', 'metrics'], + revision: 2, + updated_at: '2020-12-09T13:46:31.840Z', + updated_by: 'elastic', + agents: 100, + }, ], - total: 1, + total: 2, page: 1, perPage: 100, }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/package_policies_panel.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/package_policies_panel.tsx index 4d8cb5a16034f..c740adc4201de 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/package_policies_panel.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/package_policies_panel.tsx @@ -17,10 +17,7 @@ import { FormattedRelative, FormattedMessage } from '@kbn/i18n/react'; import { useGetPackageInstallStatus } from '../../hooks'; import { InstallStatus } from '../../../../types'; import { useLink } from '../../../../hooks'; -import { - AGENT_SAVED_OBJECT_TYPE, - PACKAGE_POLICY_SAVED_OBJECT_TYPE, -} from '../../../../../../../common/constants'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../../../common/constants'; import { useUrlPagination } from '../../../../hooks'; import { PackagePolicyAndAgentPolicy, @@ -28,6 +25,7 @@ import { } from './use_package_policies_with_agent_policy'; import { LinkAndRevision, LinkAndRevisionProps } from '../../../../components'; import { Persona } from './persona'; +import { LinkedAgentCount } from '../../../../components/linked_agent_count'; const IntegrationDetailsLink = memo<{ packagePolicy: PackagePolicyAndAgentPolicy['packagePolicy']; @@ -66,22 +64,6 @@ const AgentPolicyDetailLink = memo<{ ); }); -const PolicyAgentListLink = memo<{ agentPolicyId: string; children: ReactNode }>( - ({ agentPolicyId, children }) => { - const { getHref } = useLink(); - return ( - - {children} - - ); - } -); - interface PackagePoliciesPanelProps { name: string; version: string; @@ -156,9 +138,12 @@ export const PackagePoliciesPanel = ({ name, version }: PackagePoliciesPanelProp width: '8ch', render({ packagePolicy, agentPolicy }: PackagePolicyAndAgentPolicy) { return ( - - {agentPolicy?.agents ?? 0} - + ); }, }, diff --git a/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts b/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts index c505a234c7b2b..5f2e355ca3a47 100644 --- a/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts +++ b/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts @@ -105,29 +105,38 @@ const ThresholdRT = rt.type({ export type Threshold = rt.TypeOf; -export const CriterionRT = rt.type({ +export const criterionRT = rt.type({ field: rt.string, comparator: ComparatorRT, value: rt.union([rt.string, rt.number]), }); +export type Criterion = rt.TypeOf; -export type Criterion = rt.TypeOf; -export const criteriaRT = rt.array(CriterionRT); -export type Criteria = rt.TypeOf; +export const partialCriterionRT = rt.partial(criterionRT.props); +export type PartialCriterion = rt.TypeOf; -export const countCriteriaRT = criteriaRT; +export const countCriteriaRT = rt.array(criterionRT); export type CountCriteria = rt.TypeOf; -export const ratioCriteriaRT = rt.tuple([criteriaRT, criteriaRT]); +export const partialCountCriteriaRT = rt.array(partialCriterionRT); +export type PartialCountCriteria = rt.TypeOf; + +export const ratioCriteriaRT = rt.tuple([countCriteriaRT, countCriteriaRT]); export type RatioCriteria = rt.TypeOf; -export const TimeUnitRT = rt.union([ +export const partialRatioCriteriaRT = rt.tuple([partialCountCriteriaRT, partialCountCriteriaRT]); +export type PartialRatioCriteria = rt.TypeOf; + +export const partialCriteriaRT = rt.union([partialCountCriteriaRT, partialRatioCriteriaRT]); +export type PartialCriteria = rt.TypeOf; + +export const timeUnitRT = rt.union([ rt.literal('s'), rt.literal('m'), rt.literal('h'), rt.literal('d'), ]); -export type TimeUnit = rt.TypeOf; +export type TimeUnit = rt.TypeOf; export const timeSizeRT = rt.number; export const groupByRT = rt.array(rt.string); @@ -136,15 +145,18 @@ const RequiredAlertParamsRT = rt.type({ // NOTE: "count" would be better named as "threshold", but this would require a // migration of encrypted saved objects, so we'll keep "count" until it's problematic. count: ThresholdRT, - timeUnit: TimeUnitRT, + timeUnit: timeUnitRT, timeSize: timeSizeRT, }); +const partialRequiredAlertParamsRT = rt.partial(RequiredAlertParamsRT.props); +export type PartialRequiredAlertParams = rt.TypeOf; + const OptionalAlertParamsRT = rt.partial({ groupBy: groupByRT, }); -export const alertParamsRT = rt.intersection([ +export const countAlertParamsRT = rt.intersection([ rt.type({ criteria: countCriteriaRT, ...RequiredAlertParamsRT.props, @@ -153,8 +165,18 @@ export const alertParamsRT = rt.intersection([ ...OptionalAlertParamsRT.props, }), ]); +export type CountAlertParams = rt.TypeOf; -export type CountAlertParams = rt.TypeOf; +export const partialCountAlertParamsRT = rt.intersection([ + rt.type({ + criteria: partialCountCriteriaRT, + ...RequiredAlertParamsRT.props, + }), + rt.partial({ + ...OptionalAlertParamsRT.props, + }), +]); +export type PartialCountAlertParams = rt.TypeOf; export const ratioAlertParamsRT = rt.intersection([ rt.type({ @@ -165,13 +187,29 @@ export const ratioAlertParamsRT = rt.intersection([ ...OptionalAlertParamsRT.props, }), ]); - export type RatioAlertParams = rt.TypeOf; -export const AlertParamsRT = rt.union([alertParamsRT, ratioAlertParamsRT]); -export type AlertParams = rt.TypeOf; +export const partialRatioAlertParamsRT = rt.intersection([ + rt.type({ + criteria: partialRatioCriteriaRT, + ...RequiredAlertParamsRT.props, + }), + rt.partial({ + ...OptionalAlertParamsRT.props, + }), +]); +export type PartialRatioAlertParams = rt.TypeOf; + +export const alertParamsRT = rt.union([countAlertParamsRT, ratioAlertParamsRT]); +export type AlertParams = rt.TypeOf; + +export const partialAlertParamsRT = rt.union([ + partialCountAlertParamsRT, + partialRatioAlertParamsRT, +]); +export type PartialAlertParams = rt.TypeOf; -export const isRatioAlert = (criteria: AlertParams['criteria']): criteria is RatioCriteria => { +export const isRatioAlert = (criteria: PartialCriteria): criteria is PartialRatioCriteria => { return criteria.length > 0 && Array.isArray(criteria[0]) ? true : false; }; @@ -179,11 +217,13 @@ export const isRatioAlertParams = (params: AlertParams): params is RatioAlertPar return isRatioAlert(params.criteria); }; -export const getNumerator = (criteria: RatioCriteria): Criteria => { +export const getNumerator = (criteria: C): C[0] => { return criteria[0]; }; -export const getDenominator = (criteria: RatioCriteria): Criteria => { +export const getDenominator = ( + criteria: C +): C[1] => { return criteria[1]; }; diff --git a/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts b/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts index 3226287d4cbde..90547e6812225 100644 --- a/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts +++ b/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts @@ -6,8 +6,8 @@ import * as rt from 'io-ts'; import { - criteriaRT, - TimeUnitRT, + countCriteriaRT, + timeUnitRT, timeSizeRT, groupByRT, } from '../../alerting/logs/log_threshold/types'; @@ -42,8 +42,8 @@ export type GetLogAlertsChartPreviewDataSuccessResponsePayload = rt.TypeOf< export const getLogAlertsChartPreviewDataAlertParamsSubsetRT = rt.intersection([ rt.type({ - criteria: criteriaRT, - timeUnit: TimeUnitRT, + criteria: countCriteriaRT, + timeUnit: timeUnitRT, timeSize: timeSizeRT, }), rt.partial({ diff --git a/x-pack/plugins/infra/common/utility_types.ts b/x-pack/plugins/infra/common/utility_types.ts index 93fc9b729ca74..6bd784fed9308 100644 --- a/x-pack/plugins/infra/common/utility_types.ts +++ b/x-pack/plugins/infra/common/utility_types.ts @@ -43,3 +43,6 @@ export type DeepPartial = T extends any[] interface DeepPartialArray extends Array> {} type DeepPartialObject = { [P in keyof T]+?: DeepPartial }; + +export type ObjectEntry = [keyof T, T[keyof T]]; +export type ObjectEntries = Array>; diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criteria.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criteria.tsx index 3c474ee1d0ec6..555ac905d2963 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criteria.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criteria.tsx @@ -11,12 +11,11 @@ import { i18n } from '@kbn/i18n'; import { IFieldType } from 'src/plugins/data/public'; import { Criterion } from './criterion'; import { - AlertParams, - Comparator, - Criteria as CriteriaType, - Criterion as CriterionType, - CountCriteria as CountCriteriaType, - RatioCriteria as RatioCriteriaType, + PartialAlertParams, + PartialCountCriteria as PartialCountCriteriaType, + PartialCriteria as PartialCriteriaType, + PartialCriterion as PartialCriterionType, + PartialRatioCriteria as PartialRatioCriteriaType, isRatioAlert, getNumerator, getDenominator, @@ -25,8 +24,6 @@ import { Errors, CriterionErrors } from '../../validation'; import { ExpressionLike } from './editor'; import { CriterionPreview } from './criterion_preview_chart'; -const DEFAULT_CRITERIA = { field: 'log.level', comparator: Comparator.EQ, value: 'error' }; - const QueryAText = i18n.translate('xpack.infra.logs.alerting.threshold.ratioCriteriaQueryAText', { defaultMessage: 'Query A', }); @@ -37,11 +34,12 @@ const QueryBText = i18n.translate('xpack.infra.logs.alerting.threshold.ratioCrit interface SharedProps { fields: IFieldType[]; - criteria?: AlertParams['criteria']; + criteria?: PartialCriteriaType; + defaultCriterion: PartialCriterionType; errors: Errors['criteria']; - alertParams: Partial; + alertParams: PartialAlertParams; sourceId: string; - updateCriteria: (criteria: AlertParams['criteria']) => void; + updateCriteria: (criteria: PartialCriteriaType) => void; } type CriteriaProps = SharedProps; @@ -60,10 +58,10 @@ export const Criteria: React.FC = (props) => { interface CriteriaWrapperProps { alertParams: SharedProps['alertParams']; fields: SharedProps['fields']; - updateCriterion: (idx: number, params: Partial) => void; + updateCriterion: (idx: number, params: PartialCriterionType) => void; removeCriterion: (idx: number) => void; addCriterion: () => void; - criteria: CriteriaType; + criteria: PartialCountCriteriaType; errors: CriterionErrors; sourceId: SharedProps['sourceId']; isRatio?: boolean; @@ -118,29 +116,24 @@ const CriteriaWrapper: React.FC = (props) => { ); }; -interface RatioCriteriaProps { - alertParams: SharedProps['alertParams']; - fields: SharedProps['fields']; - criteria: RatioCriteriaType; - errors: Errors['criteria']; - sourceId: SharedProps['sourceId']; - updateCriteria: (criteria: AlertParams['criteria']) => void; +interface RatioCriteriaProps extends SharedProps { + criteria: PartialRatioCriteriaType; } const RatioCriteria: React.FC = (props) => { - const { criteria, errors, updateCriteria } = props; + const { criteria, defaultCriterion, errors, updateCriteria } = props; const handleUpdateNumeratorCriteria = useCallback( - (criteriaParam: CriteriaType) => { - const nextCriteria: RatioCriteriaType = [criteriaParam, getDenominator(criteria)]; + (criteriaParam: PartialCountCriteriaType) => { + const nextCriteria: PartialRatioCriteriaType = [criteriaParam, getDenominator(criteria)]; updateCriteria(nextCriteria); }, [updateCriteria, criteria] ); const handleUpdateDenominatorCriteria = useCallback( - (criteriaParam: CriteriaType) => { - const nextCriteria: RatioCriteriaType = [getNumerator(criteria), criteriaParam]; + (criteriaParam: PartialCountCriteriaType) => { + const nextCriteria: PartialRatioCriteriaType = [getNumerator(criteria), criteriaParam]; updateCriteria(nextCriteria); }, [updateCriteria, criteria] @@ -150,13 +143,13 @@ const RatioCriteria: React.FC = (props) => { updateCriterion: updateNumeratorCriterion, addCriterion: addNumeratorCriterion, removeCriterion: removeNumeratorCriterion, - } = useCriteriaState(getNumerator(criteria), handleUpdateNumeratorCriteria); + } = useCriteriaState(getNumerator(criteria), defaultCriterion, handleUpdateNumeratorCriteria); const { updateCriterion: updateDenominatorCriterion, addCriterion: addDenominatorCriterion, removeCriterion: removeDenominatorCriterion, - } = useCriteriaState(getDenominator(criteria), handleUpdateDenominatorCriteria); + } = useCriteriaState(getDenominator(criteria), defaultCriterion, handleUpdateDenominatorCriteria); return ( <> @@ -191,28 +184,17 @@ const RatioCriteria: React.FC = (props) => { ); }; -interface CountCriteriaProps { - alertParams: SharedProps['alertParams']; - fields: SharedProps['fields']; - criteria: CountCriteriaType; - errors: Errors['criteria']; - sourceId: SharedProps['sourceId']; - updateCriteria: (criteria: AlertParams['criteria']) => void; +interface CountCriteriaProps extends SharedProps { + criteria: PartialCountCriteriaType; } const CountCriteria: React.FC = (props) => { - const { criteria, updateCriteria, errors } = props; - - const handleUpdateCriteria = useCallback( - (criteriaParam: CriteriaType) => { - updateCriteria(criteriaParam); - }, - [updateCriteria] - ); + const { criteria, defaultCriterion, updateCriteria, errors } = props; const { updateCriterion, addCriterion, removeCriterion } = useCriteriaState( criteria, - handleUpdateCriteria + defaultCriterion, + updateCriteria ); return ( @@ -227,8 +209,9 @@ const CountCriteria: React.FC = (props) => { }; const useCriteriaState = ( - criteria: CriteriaType, - onUpdateCriteria: (criteria: CriteriaType) => void + criteria: PartialCountCriteriaType, + defaultCriterion: PartialCriterionType, + onUpdateCriteria: (criteria: PartialCountCriteriaType) => void ) => { const updateCriterion = useCallback( (idx, criterionParams) => { @@ -241,13 +224,13 @@ const useCriteriaState = ( ); const addCriterion = useCallback(() => { - const nextCriteria = criteria ? [...criteria, DEFAULT_CRITERIA] : [DEFAULT_CRITERIA]; + const nextCriteria = [...criteria, defaultCriterion]; onUpdateCriteria(nextCriteria); - }, [criteria, onUpdateCriteria]); + }, [criteria, defaultCriterion, onUpdateCriteria]); const removeCriterion = useCallback( (idx) => { - const nextCriteria = criteria.filter((criterion, index) => { + const nextCriteria = criteria.filter((_criterion, index) => { return index !== idx; }); onUpdateCriteria(nextCriteria); diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion.tsx index b2992ead3ea1b..9763a973d2fbd 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion.tsx @@ -90,7 +90,7 @@ const getFieldInfo = (fields: IFieldType[], fieldName: string): IFieldType | und interface Props { idx: number; fields: IFieldType[]; - criterion: CriterionType; + criterion: Partial; updateCriterion: (idx: number, params: Partial) => void; removeCriterion: (idx: number) => void; canDelete: boolean; @@ -116,7 +116,11 @@ export const Criterion: React.FC = ({ }, [fields]); const fieldInfo: IFieldType | undefined = useMemo(() => { - return getFieldInfo(fields, criterion.field); + if (criterion.field) { + return getFieldInfo(fields, criterion.field); + } else { + return undefined; + } }, [fields, criterion]); const compatibleComparatorOptions = useMemo(() => { @@ -129,10 +133,8 @@ export const Criterion: React.FC = ({ const nextFieldInfo = getFieldInfo(fields, fieldName); // If the field information we're dealing with has changed, reset the comparator and value. if ( - fieldInfo && - nextFieldInfo && - (fieldInfo.type !== nextFieldInfo.type || - fieldInfo.aggregatable !== nextFieldInfo.aggregatable) + fieldInfo?.type !== nextFieldInfo?.type || + fieldInfo?.aggregatable !== nextFieldInfo?.aggregatable ) { const compatibleComparators = getCompatibleComparatorsForField(nextFieldInfo); updateCriterion(idx, { @@ -160,7 +162,7 @@ export const Criterion: React.FC = ({ idx === 0 ? firstCriterionFieldPrefix : successiveCriterionFieldPrefix } uppercase={true} - value={criterion.field} + value={criterion.field ?? 'a chosen field'} isActive={isFieldPopoverOpen} color={errors.field.length === 0 ? 'secondary' : 'danger'} onClick={(e) => { @@ -180,7 +182,8 @@ export const Criterion: React.FC = ({ 0} error={errors.field}> @@ -194,9 +197,11 @@ export const Criterion: React.FC = ({ button={ = ({ 0} error={errors.comparator}> updateCriterion(idx, { comparator: e.target.value as Comparator }) diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx index 47dc419022880..cb759afa66d5c 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx @@ -34,7 +34,7 @@ import { NUM_BUCKETS, } from '../../../common/criterion_preview_chart/criterion_preview_chart'; import { - AlertParams, + PartialAlertParams, Threshold, Criterion, Comparator, @@ -50,7 +50,7 @@ import { decodeOrThrow } from '../../../../../common/runtime_types'; const GROUP_LIMIT = 5; interface Props { - alertParams: Partial; + alertParams: PartialAlertParams; chartCriterion: Partial; sourceId: string; showThreshold: boolean; diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx index 854363aacca5c..f69ca798c01b0 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx @@ -4,25 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useMemo, useState } from 'react'; +import { EuiButton, EuiCallOut, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { EuiLoadingSpinner, EuiSpacer, EuiButton, EuiCallOut } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; import useMount from 'react-use/lib/useMount'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -import { GroupByExpression } from '../../../common/group_by_expression/group_by_expression'; -import { ForLastExpression } from '../../../../../../triggers_actions_ui/public'; import { - AlertParams, + AlertTypeParamsExpressionProps, + ForLastExpression, +} from '../../../../../../triggers_actions_ui/public'; +import { + PartialAlertParams, Comparator, - ThresholdType, isRatioAlert, + PartialCriteria as PartialCriteriaType, + ThresholdType, + timeUnitRT, } from '../../../../../common/alerting/logs/log_threshold/types'; -import { Threshold } from './threshold'; +import { decodeOrThrow } from '../../../../../common/runtime_types'; +import { ObjectEntries } from '../../../../../common/utility_types'; +import { + LogIndexField, + LogSourceProvider, + useLogSourceContext, +} from '../../../../containers/logs/log_source'; +import { useSourceId } from '../../../../containers/source_id'; +import { GroupByExpression } from '../../../common/group_by_expression/group_by_expression'; +import { errorsRT } from '../../validation'; import { Criteria } from './criteria'; +import { Threshold } from './threshold'; import { TypeSwitcher } from './type_switcher'; -import { useSourceId } from '../../../../containers/source_id'; -import { LogSourceProvider, useLogSourceContext } from '../../../../containers/logs/log_source'; -import { Errors } from '../../validation'; export interface ExpressionCriteria { field?: string; @@ -34,45 +45,46 @@ interface LogsContextMeta { isInternal?: boolean; } -interface Props { - errors: Errors; - alertParams: Partial; - setAlertParams(key: string, value: any): void; - setAlertProperty(key: string, value: any): void; - sourceId: string; - metadata: LogsContextMeta; -} - -const DEFAULT_CRITERIA = { field: 'log.level', comparator: Comparator.EQ, value: 'error' }; - const DEFAULT_BASE_EXPRESSION = { timeSize: 5, timeUnit: 'm', }; -const DEFAULT_COUNT_EXPRESSION = { +const DEFAULT_FIELD = 'log.level'; + +const createDefaultCriterion = ( + availableFields: LogIndexField[], + value: ExpressionCriteria['value'] +) => + availableFields.some((availableField) => availableField.name === DEFAULT_FIELD) + ? { field: DEFAULT_FIELD, comparator: Comparator.EQ, value } + : { field: undefined, comparator: undefined, value: undefined }; + +const createDefaultCountAlertParams = (availableFields: LogIndexField[]) => ({ ...DEFAULT_BASE_EXPRESSION, count: { value: 75, comparator: Comparator.GT, }, - criteria: [DEFAULT_CRITERIA], -}; + criteria: [createDefaultCriterion(availableFields, 'error')], +}); -const DEFAULT_RATIO_EXPRESSION = { +const createDefaultRatioAlertParams = (availableFields: LogIndexField[]) => ({ ...DEFAULT_BASE_EXPRESSION, count: { value: 2, comparator: Comparator.GT, }, criteria: [ - [DEFAULT_CRITERIA], - [{ field: 'log.level', comparator: Comparator.EQ, value: 'warning' }], + createDefaultCriterion(availableFields, 'error'), + createDefaultCriterion([], 'warning'), ], -}; +}); -export const ExpressionEditor: React.FC = (props) => { - const isInternal = props.metadata?.isInternal; +export const ExpressionEditor: React.FC< + AlertTypeParamsExpressionProps +> = (props) => { + const isInternal = props.metadata?.isInternal ?? false; const [sourceId] = useSourceId(); const { http } = useKibana().services; @@ -80,12 +92,12 @@ export const ExpressionEditor: React.FC = (props) => { <> {isInternal ? ( - + ) : ( - + )} @@ -93,7 +105,7 @@ export const ExpressionEditor: React.FC = (props) => { ); }; -export const SourceStatusWrapper: React.FC = (props) => { +export const SourceStatusWrapper: React.FC = ({ children }) => { const { initialize, isLoadingSourceStatus, @@ -101,7 +113,6 @@ export const SourceStatusWrapper: React.FC = (props) => { hasFailedLoadingSourceStatus, loadSourceStatus, } = useLogSourceContext(); - const { children } = props; useMount(() => { initialize(); @@ -136,16 +147,19 @@ export const SourceStatusWrapper: React.FC = (props) => { ); }; -export const Editor: React.FC = (props) => { - const { setAlertParams, alertParams, errors, sourceId } = props; +export const Editor: React.FC< + AlertTypeParamsExpressionProps +> = (props) => { + const { setAlertParams, alertParams, errors } = props; const [hasSetDefaults, setHasSetDefaults] = useState(false); - const { sourceStatus } = useLogSourceContext(); - useMount(() => { - for (const [key, value] of Object.entries({ ...DEFAULT_COUNT_EXPRESSION, ...alertParams })) { - setAlertParams(key, value); - } - setHasSetDefaults(true); - }); + const { sourceId, sourceStatus } = useLogSourceContext(); + + const { + criteria: criteriaErrors, + threshold: thresholdErrors, + timeSizeUnit: timeSizeUnitErrors, + timeWindowSize: timeWindowSizeErrors, + } = useMemo(() => decodeOrThrow(errorsRT)(errors), [errors]); const supportedFields = useMemo(() => { if (sourceStatus?.logIndexFields) { @@ -176,7 +190,7 @@ export const Editor: React.FC = (props) => { ); const updateCriteria = useCallback( - (criteria: AlertParams['criteria']) => { + (criteria: PartialCriteriaType) => { setAlertParams('criteria', criteria); }, [setAlertParams] @@ -191,7 +205,9 @@ export const Editor: React.FC = (props) => { const updateTimeUnit = useCallback( (tu: string) => { - setAlertParams('timeUnit', tu); + if (timeUnitRT.is(tu)) { + setAlertParams('timeUnit', tu); + } }, [setAlertParams] ); @@ -203,20 +219,31 @@ export const Editor: React.FC = (props) => { [setAlertParams] ); + const defaultCountAlertParams = useMemo(() => createDefaultCountAlertParams(supportedFields), [ + supportedFields, + ]); + const updateType = useCallback( (type: ThresholdType) => { - const defaults = type === 'count' ? DEFAULT_COUNT_EXPRESSION : DEFAULT_RATIO_EXPRESSION; + const defaults = + type === 'count' ? defaultCountAlertParams : createDefaultRatioAlertParams(supportedFields); // Reset properties that don't make sense switching from one context to the other - for (const [key, value] of Object.entries({ - criteria: defaults.criteria, - count: defaults.count, - })) { - setAlertParams(key, value); - } + setAlertParams('count', defaults.count); + setAlertParams('criteria', defaults.criteria); }, - [setAlertParams] + [defaultCountAlertParams, setAlertParams, supportedFields] ); + useMount(() => { + const newAlertParams = { ...defaultCountAlertParams, ...alertParams }; + for (const [key, value] of Object.entries(newAlertParams) as ObjectEntries< + typeof newAlertParams + >) { + setAlertParams(key, value); + } + setHasSetDefaults(true); + }); + // Wait until the alert param defaults have been set if (!hasSetDefaults) return null; @@ -224,7 +251,8 @@ export const Editor: React.FC = (props) => { = (props) => { comparator={alertParams.count?.comparator} value={alertParams.count?.value} updateThreshold={updateThreshold} - errors={errors.threshold} + errors={thresholdErrors} /> = (props) => { timeWindowUnit={alertParams.timeUnit} onChangeWindowSize={updateTimeSize} onChangeWindowUnit={updateTimeUnit} - errors={{ timeWindowSize: errors.timeWindowSize, timeSizeUnit: errors.timeSizeUnit }} + errors={{ timeWindowSize: timeWindowSizeErrors, timeSizeUnit: timeSizeUnitErrors }} /> void; } -const getThresholdType = (criteria: AlertParams['criteria']): ThresholdType => { +const getThresholdType = (criteria: PartialCriteria): ThresholdType => { return isRatioAlert(criteria) ? 'ratio' : 'count'; }; diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts index 7154a77496b81..6cdb81155ec9a 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts +++ b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts @@ -6,10 +6,13 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; -import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../../../common/alerting/logs/log_threshold/types'; +import { + LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, + PartialAlertParams, +} from '../../../common/alerting/logs/log_threshold/types'; import { validateExpression } from './validation'; -export function getAlertType(): AlertTypeModel { +export function getAlertType(): AlertTypeModel { return { id: LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, description: i18n.translate('xpack.infra.logs.alertFlyout.alertDescription', { diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/validation.ts b/x-pack/plugins/infra/public/alerting/log_threshold/validation.ts index 6630b3d079141..24d373558008d 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/validation.ts +++ b/x-pack/plugins/infra/public/alerting/log_threshold/validation.ts @@ -5,45 +5,53 @@ */ import { i18n } from '@kbn/i18n'; +import * as rt from 'io-ts'; import { isNumber, isFinite } from 'lodash'; -import { ValidationResult } from '../../../../triggers_actions_ui/public'; +import { IErrorObject, ValidationResult } from '../../../../triggers_actions_ui/public'; import { - AlertParams, - Criteria, - RatioCriteria, + PartialCountCriteria, isRatioAlert, getNumerator, getDenominator, + PartialRequiredAlertParams, + PartialCriteria, } from '../../../common/alerting/logs/log_threshold/types'; -export interface CriterionErrors { - [id: string]: { - field: string[]; - comparator: string[]; - value: string[]; - }; -} +export const criterionErrorRT = rt.type({ + field: rt.array(rt.string), + comparator: rt.array(rt.string), + value: rt.array(rt.string), +}); -export interface Errors { - threshold: { - value: string[]; - }; +export const criterionErrorsRT = rt.record(rt.string, criterionErrorRT); + +export type CriterionErrors = rt.TypeOf; + +const alertingErrorRT: rt.Type = rt.recursion('AlertingError', () => + rt.record(rt.string, rt.union([rt.string, rt.array(rt.string), alertingErrorRT])) +); + +export const errorsRT = rt.type({ + threshold: rt.type({ + value: rt.array(rt.string), + }), // NOTE: The data structure for criteria errors isn't 100% // ideal but we need to conform to the interfaces that the alerting // framework expects. - criteria: { - [id: string]: CriterionErrors; - }; - timeWindowSize: string[]; - timeSizeUnit: string[]; -} + criteria: rt.record(rt.string, criterionErrorsRT), + timeWindowSize: rt.array(rt.string), + timeSizeUnit: rt.array(rt.string), +}); + +export type Errors = rt.TypeOf; export function validateExpression({ count, criteria, timeSize, - timeUnit, -}: Partial): ValidationResult { +}: PartialRequiredAlertParams & { + criteria: PartialCriteria; +}): ValidationResult { const validationResult = { errors: {} }; // NOTE: In the case of components provided by the Alerting framework the error property names @@ -79,7 +87,7 @@ export function validateExpression({ // Criteria validation if (criteria && criteria.length > 0) { - const getCriterionErrors = (_criteria: Criteria): CriterionErrors => { + const getCriterionErrors = (_criteria: PartialCountCriteria): CriterionErrors => { const _errors: CriterionErrors = {}; _criteria.forEach((criterion, idx) => { @@ -114,12 +122,12 @@ export function validateExpression({ }; if (!isRatioAlert(criteria)) { - const criteriaErrors = getCriterionErrors(criteria as Criteria); + const criteriaErrors = getCriterionErrors(criteria); errors.criteria[0] = criteriaErrors; } else { - const numeratorErrors = getCriterionErrors(getNumerator(criteria as RatioCriteria)); + const numeratorErrors = getCriterionErrors(getNumerator(criteria)); errors.criteria[0] = numeratorErrors; - const denominatorErrors = getCriterionErrors(getDenominator(criteria as RatioCriteria)); + const denominatorErrors = getCriterionErrors(getDenominator(criteria)); errors.criteria[1] = denominatorErrors; } } diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts index 879d2d95d7946..d7f40f603a9f7 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts @@ -9,6 +9,7 @@ import { useCallback, useMemo, useState } from 'react'; import useMountedState from 'react-use/lib/useMountedState'; import type { HttpHandler } from 'src/core/public'; import { + LogIndexField, LogSourceConfiguration, LogSourceConfigurationProperties, LogSourceConfigurationPropertiesPatch, @@ -20,6 +21,7 @@ import { callFetchLogSourceStatusAPI } from './api/fetch_log_source_status'; import { callPatchLogSourceConfigurationAPI } from './api/patch_log_source_configuration'; export { + LogIndexField, LogSourceConfiguration, LogSourceConfigurationProperties, LogSourceConfigurationPropertiesPatch, diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx index 98367335d9c2d..6fc9ce3d8983e 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx @@ -178,21 +178,19 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent - - - - - - - - + + + + + + const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY); try { - const validatedParams = decodeOrThrow(AlertParamsRT)(params); + const validatedParams = decodeOrThrow(alertParamsRT)(params); if (!isRatioAlertParams(validatedParams)) { await executeAlert( @@ -174,7 +174,7 @@ async function executeRatioAlert( } const getESQuery = ( - alertParams: Omit & { criteria: Criteria }, + alertParams: Omit & { criteria: CountCriteria }, timestampField: string, indexPattern: string ) => { @@ -366,7 +366,7 @@ export const updateAlertInstance: AlertInstanceUpdater = (alertInstance, state, }; export const buildFiltersFromCriteria = ( - params: Pick & { criteria: Criteria }, + params: Pick & { criteria: CountCriteria }, timestampField: string ) => { const { timeSize, timeUnit, criteria } = params; @@ -417,7 +417,7 @@ export const buildFiltersFromCriteria = ( }; export const getGroupedESQuery = ( - params: Pick & { criteria: Criteria }, + params: Pick & { criteria: CountCriteria }, timestampField: string, index: string ): object | undefined => { @@ -475,7 +475,7 @@ export const getGroupedESQuery = ( }; export const getUngroupedESQuery = ( - params: Pick & { criteria: Criteria }, + params: Pick & { criteria: CountCriteria }, timestampField: string, index: string ): object => { @@ -509,7 +509,7 @@ type Filter = { [key in SupportedESQueryTypes]?: object; }; -const buildFiltersForCriteria = (criteria: Criteria) => { +const buildFiltersForCriteria = (criteria: CountCriteria) => { let filters: Filter[] = []; criteria.forEach((criterion) => { @@ -643,7 +643,7 @@ const getGroupedResults = async ( return compositeGroupBuckets; }; -const createConditionsMessageForCriteria = (criteria: Criteria) => { +const createConditionsMessageForCriteria = (criteria: CountCriteria) => { const parts = criteria.map((criterion, index) => { const { field, comparator, value } = criterion; return `${index === 0 ? '' : 'and'} ${field} ${comparator} ${value}`; diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts index 4703371f5e0de..e248d3b3ddcfa 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts @@ -8,7 +8,7 @@ import { PluginSetupContract } from '../../../../../alerts/server'; import { createLogThresholdExecutor, FIRED_ACTIONS } from './log_threshold_executor'; import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, - AlertParamsRT, + alertParamsRT, } from '../../../../common/alerting/logs/log_threshold/types'; import { InfraBackendLibs } from '../../infra_types'; import { decodeOrThrow } from '../../../../common/runtime_types'; @@ -86,7 +86,7 @@ export async function registerLogThresholdAlertType( }), validate: { params: { - validate: (params) => decodeOrThrow(AlertParamsRT)(params), + validate: (params) => decodeOrThrow(alertParamsRT)(params), }, }, defaultActionGroupId: FIRED_ACTIONS.id, diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index b00760e9664f3..ea7ce99e92cef 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -260,6 +260,7 @@ export class Embeddable handleEvent={this.handleEvent} onData$={this.updateActiveData} renderMode={input.renderMode} + syncColors={input.syncColors} hasCompatibleActions={this.hasCompatibleActions} />, domNode diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx index e2607886a4219..c91ca74b54a4f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx @@ -29,6 +29,7 @@ export interface ExpressionWrapperProps { inspectorAdapters?: Partial | undefined ) => void; renderMode?: RenderMode; + syncColors?: boolean; hasCompatibleActions?: ReactExpressionRendererProps['hasCompatibleActions']; } @@ -41,6 +42,7 @@ export function ExpressionWrapper({ searchSessionId, onData$, renderMode, + syncColors, hasCompatibleActions, }: ExpressionWrapperProps) { return ( @@ -70,6 +72,7 @@ export function ExpressionWrapper({ searchSessionId={searchSessionId} onData$={onData$} renderMode={renderMode} + syncColors={syncColors} renderError={(errorMessage, error) => (
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 95a6c351e1fc2..5d477d98d042d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -1329,8 +1329,10 @@ describe('IndexPatternDimensionEditorPanel', () => { 'Median', 'Minimum', 'Moving average', + 'Percentile', 'Sum', 'Unique count', + '\u00a0', ]); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 7e67d863346c7..1f23fd3830477 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -474,6 +474,53 @@ describe('IndexPattern Data Source', () => { expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp', 'another_datefield']); }); + it('should add the suffix to the remap column id if provided by the operation', async () => { + const queryBaseState: IndexPatternBaseState = { + currentIndexPatternId: '1', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['def', 'abc'], + columns: { + abc: { + label: '23rd percentile', + dataType: 'number', + isBucketed: false, + sourceField: 'bytes', + operationType: 'percentile', + params: { + percentile: 23, + }, + }, + def: { + label: 'Terms', + dataType: 'string', + isBucketed: true, + operationType: 'terms', + sourceField: 'source', + params: { + size: 5, + orderBy: { + type: 'alphabetical', + }, + orderDirection: 'asc', + }, + }, + }, + }, + }, + }; + + const state = enrichBaseState(queryBaseState); + + const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + expect(Object.keys(JSON.parse(ast.chain[1].arguments.idMap[0] as string))).toEqual([ + 'col-0-def', + // col-1 is the auto naming of esasggs, abc is the specified column id, .23 is the generated suffix + 'col-1-abc.23', + ]); + }); + it('should add time_scale and format function if time scale is set and supported', async () => { const queryBaseState: IndexPatternBaseState = { currentIndexPatternId: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx index e1bc378635f1d..d9805b337c000 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx @@ -19,7 +19,7 @@ import { hasDateField, } from './utils'; import { updateColumnParam } from '../../layer_helpers'; -import { useDebounceWithOptions } from '../helpers'; +import { isValidNumber, useDebounceWithOptions } from '../helpers'; import { adjustTimeScaleOnOtherColumnChange } from '../../time_scale_utils'; import type { OperationDefinition, ParamEditorProps } from '..'; @@ -122,19 +122,6 @@ export const movingAverageOperation: OperationDefinition< timeScalingMode: 'optional', }; -function isValidNumber(input: string) { - if (input === '') return false; - try { - const val = parseFloat(input); - if (isNaN(val)) return false; - if (val < 1) return false; - if (val.toString().includes('.')) return false; - } catch (e) { - return false; - } - return true; -} - function MovingAverageParamEditor({ layer, updateLayer, @@ -145,7 +132,7 @@ function MovingAverageParamEditor({ useDebounceWithOptions( () => { - if (!isValidNumber(inputValue)) return; + if (!isValidNumber(inputValue, true, undefined, 1)) return; const inputNumber = parseInt(inputValue, 10); updateLayer( updateColumnParam({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx index 8c2fa43b541d4..abd033c0db4cf 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx @@ -199,7 +199,8 @@ describe('date_histogram', () => { const esAggsFn = dateHistogramOperation.toEsAggsFn( layer.columns.col1 as DateHistogramIndexPatternColumn, 'col1', - indexPattern1 + indexPattern1, + layer ); expect(esAggsFn).toEqual( expect.objectContaining({ @@ -250,7 +251,8 @@ describe('date_histogram', () => { }, }, ]), - } + }, + layer ); expect(esAggsFn).toEqual( expect.objectContaining({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx index cf57c35f6f68b..86767fbc8b469 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx @@ -83,7 +83,8 @@ describe('filters', () => { const esAggsFn = filtersOperation.toEsAggsFn( layer.columns.col1 as FiltersIndexPatternColumn, 'col1', - createMockedIndexPattern() + createMockedIndexPattern(), + layer ); expect(esAggsFn).toEqual( expect.objectContaining({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx index 7b96bcf4f2069..29148052cee8e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx @@ -7,7 +7,7 @@ import { useRef } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; import { i18n } from '@kbn/i18n'; -import { operationDefinitionMap } from '.'; +import { IndexPatternColumn, operationDefinitionMap } from '.'; import { FieldBasedIndexPatternColumn } from './column_types'; import { IndexPattern } from '../../types'; @@ -63,6 +63,13 @@ export function getInvalidFieldMessage( : undefined; } +export function getEsAggsSuffix(column: IndexPatternColumn) { + const operationDefinition = operationDefinitionMap[column.operationType]; + return operationDefinition.input === 'field' && operationDefinition.getEsAggsSuffix + ? operationDefinition.getEsAggsSuffix(column) + : ''; +} + export function getSafeName(name: string, indexPattern: IndexPattern): string { const field = indexPattern.getFieldByName(name); return field @@ -71,3 +78,22 @@ export function getSafeName(name: string, indexPattern: IndexPattern): string { defaultMessage: 'Missing field', }); } + +export function isValidNumber( + inputValue: string | number | null | undefined, + integer?: boolean, + upperBound?: number, + lowerBound?: number +) { + const inputValueAsNumber = Number(inputValue); + return ( + inputValue !== '' && + inputValue !== null && + inputValue !== undefined && + !Number.isNaN(inputValueAsNumber) && + Number.isFinite(inputValueAsNumber) && + (!integer || Number.isInteger(inputValueAsNumber)) && + (upperBound === undefined || inputValueAsNumber <= upperBound) && + (lowerBound === undefined || inputValueAsNumber >= lowerBound) + ); +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 6231460347de2..36c9cf75d2b6c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -9,6 +9,7 @@ import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { termsOperation, TermsIndexPatternColumn } from './terms'; import { filtersOperation, FiltersIndexPatternColumn } from './filters'; import { cardinalityOperation, CardinalityIndexPatternColumn } from './cardinality'; +import { percentileOperation, PercentileIndexPatternColumn } from './percentile'; import { minOperation, MinIndexPatternColumn, @@ -58,6 +59,7 @@ export type IndexPatternColumn = | CardinalityIndexPatternColumn | SumIndexPatternColumn | MedianIndexPatternColumn + | PercentileIndexPatternColumn | CountIndexPatternColumn | LastValueIndexPatternColumn | CumulativeSumIndexPatternColumn @@ -82,6 +84,7 @@ const internalOperationDefinitions = [ cardinalityOperation, sumOperation, medianOperation, + percentileOperation, lastValueOperation, countOperation, rangeOperation, @@ -96,6 +99,7 @@ export { rangeOperation } from './ranges'; export { filtersOperation } from './filters'; export { dateHistogramOperation } from './date_histogram'; export { minOperation, averageOperation, sumOperation, maxOperation } from './metrics'; +export { percentileOperation } from './percentile'; export { countOperation } from './count'; export { lastValueOperation } from './last_value'; export { @@ -223,7 +227,12 @@ interface FieldlessOperationDefinition { * Function turning a column into an agg config passed to the `esaggs` function * together with the agg configs returned from other columns. */ - toEsAggsFn: (column: C, columnId: string, indexPattern: IndexPattern) => ExpressionAstFunction; + toEsAggsFn: ( + column: C, + columnId: string, + indexPattern: IndexPattern, + layer: IndexPatternLayer + ) => ExpressionAstFunction; } interface FieldBasedOperationDefinition { @@ -262,7 +271,19 @@ interface FieldBasedOperationDefinition { * Function turning a column into an agg config passed to the `esaggs` function * together with the agg configs returned from other columns. */ - toEsAggsFn: (column: C, columnId: string, indexPattern: IndexPattern) => ExpressionAstFunction; + toEsAggsFn: ( + column: C, + columnId: string, + indexPattern: IndexPattern, + layer: IndexPatternLayer + ) => ExpressionAstFunction; + /** + * Optional function to return the suffix used for ES bucket paths and esaggs column id. + * This is relevant for multi metrics to pick the right value. + * + * @param column The current column + */ + getEsAggsSuffix?: (column: C) => string; /** * Validate that the operation has the right preconditions in the state. For example: * diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx index bde8cd0e42427..96b12a714e613 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx @@ -69,7 +69,8 @@ describe('last_value', () => { const esAggsFn = lastValueOperation.toEsAggsFn( { ...lastValueColumn, params: { ...lastValueColumn.params } }, 'col1', - {} as IndexPattern + {} as IndexPattern, + layer ); expect(esAggsFn).toEqual( expect.objectContaining({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx index eb25b5d932b1f..470a5407b2589 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx @@ -83,21 +83,24 @@ function buildMetricOperation>({ }, onOtherColumnChanged: (layer, thisColumnId, changedColumnId) => optionalTimeScaling - ? adjustTimeScaleOnOtherColumnChange(layer, thisColumnId, changedColumnId) - : layer.columns[thisColumnId], + ? (adjustTimeScaleOnOtherColumnChange(layer, thisColumnId, changedColumnId) as T) + : (layer.columns[thisColumnId] as T), getDefaultLabel: (column, indexPattern, columns) => labelLookup(getSafeName(column.sourceField, indexPattern), column), - buildColumn: ({ field, previousColumn }) => ({ - label: labelLookup(field.displayName, previousColumn), - dataType: 'number', - operationType: type, - sourceField: field.name, - isBucketed: false, - scale: 'ratio', - timeScale: optionalTimeScaling ? previousColumn?.timeScale : undefined, - params: - previousColumn && previousColumn.dataType === 'number' ? previousColumn.params : undefined, - }), + buildColumn: ({ field, previousColumn }) => + ({ + label: labelLookup(field.displayName, previousColumn), + dataType: 'number', + operationType: type, + sourceField: field.name, + isBucketed: false, + scale: 'ratio', + timeScale: optionalTimeScaling ? previousColumn?.timeScale : undefined, + params: + previousColumn && previousColumn.dataType === 'number' + ? previousColumn.params + : undefined, + } as T), onFieldChange: (oldColumn, field) => { return { ...oldColumn, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx new file mode 100644 index 0000000000000..c22eec62ea1ab --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx @@ -0,0 +1,237 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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, mount } from 'enzyme'; +import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; +import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; +import { createMockedIndexPattern } from '../../mocks'; +import { percentileOperation } from './index'; +import { IndexPattern, IndexPatternLayer } from '../../types'; +import { PercentileIndexPatternColumn } from './percentile'; +import { EuiFieldNumber } from '@elastic/eui'; +import { act } from 'react-dom/test-utils'; +import { EuiFormRow } from '@elastic/eui'; + +const defaultProps = { + storage: {} as IStorageWrapper, + uiSettings: {} as IUiSettingsClient, + savedObjectsClient: {} as SavedObjectsClientContract, + dateRange: { fromDate: 'now-1d', toDate: 'now' }, + data: dataPluginMock.createStartContract(), + http: {} as HttpSetup, + indexPattern: { + ...createMockedIndexPattern(), + hasRestrictions: false, + } as IndexPattern, +}; + +describe('percentile', () => { + let layer: IndexPatternLayer; + const InlineOptions = percentileOperation.paramEditor!; + + beforeEach(() => { + layer = { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + label: 'Top value of category', + dataType: 'string', + isBucketed: true, + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + size: 3, + orderDirection: 'asc', + }, + sourceField: 'category', + }, + col2: { + label: '23rd percentile of a', + dataType: 'number', + isBucketed: false, + sourceField: 'a', + operationType: 'percentile', + params: { + percentile: 23, + }, + }, + }, + }; + }); + + describe('toEsAggsFn', () => { + it('should reflect params correctly', () => { + const percentileColumn = layer.columns.col2 as PercentileIndexPatternColumn; + const esAggsFn = percentileOperation.toEsAggsFn( + percentileColumn, + 'col1', + {} as IndexPattern, + layer + ); + expect(esAggsFn).toEqual( + expect.objectContaining({ + arguments: expect.objectContaining({ + percents: [23], + field: ['a'], + }), + }) + ); + }); + }); + + describe('onFieldChange', () => { + it('should change correctly to new field', () => { + const oldColumn: PercentileIndexPatternColumn = { + operationType: 'percentile', + sourceField: 'bytes', + label: '23rd percentile of bytes', + isBucketed: true, + dataType: 'number', + params: { + percentile: 23, + }, + }; + const indexPattern = createMockedIndexPattern(); + const newNumberField = indexPattern.getFieldByName('memory')!; + const column = percentileOperation.onFieldChange(oldColumn, newNumberField); + + expect(column).toEqual( + expect.objectContaining({ + dataType: 'number', + sourceField: 'memory', + params: expect.objectContaining({ + percentile: 23, + }), + }) + ); + expect(column.label).toContain('memory'); + }); + }); + + describe('buildColumn', () => { + it('should set default percentile', () => { + const indexPattern = createMockedIndexPattern(); + const bytesField = indexPattern.fields.find(({ name }) => name === 'bytes')!; + bytesField.displayName = 'test'; + const percentileColumn = percentileOperation.buildColumn({ + indexPattern, + field: bytesField, + layer: { columns: {}, columnOrder: [], indexPatternId: '' }, + }); + expect(percentileColumn.dataType).toEqual('number'); + expect(percentileColumn.params.percentile).toEqual(95); + expect(percentileColumn.label).toEqual('95th percentile of test'); + }); + }); + + describe('param editor', () => { + it('should render current percentile', () => { + const updateLayerSpy = jest.fn(); + const instance = shallow( + + ); + + const input = instance.find('[data-test-subj="lns-indexPattern-percentile-input"]'); + + expect(input.prop('value')).toEqual('23'); + }); + + it('should update state on change', async () => { + jest.useFakeTimers(); + const updateLayerSpy = jest.fn(); + const instance = mount( + + ); + + jest.runAllTimers(); + + const input = instance + .find('[data-test-subj="lns-indexPattern-percentile-input"]') + .find(EuiFieldNumber); + + await act(async () => { + input.prop('onChange')!({ target: { value: '27' } } as React.ChangeEvent); + }); + + instance.update(); + + jest.runAllTimers(); + + expect(updateLayerSpy).toHaveBeenCalledWith({ + ...layer, + columns: { + ...layer.columns, + col2: { + ...layer.columns.col2, + params: { + percentile: 27, + }, + label: '27th percentile of a', + }, + }, + }); + }); + + it('should not update on invalid input, but show invalid value locally', async () => { + const updateLayerSpy = jest.fn(); + const instance = mount( + + ); + + jest.runAllTimers(); + + const input = instance + .find('[data-test-subj="lns-indexPattern-percentile-input"]') + .find(EuiFieldNumber); + + await act(async () => { + input.prop('onChange')!({ + target: { value: '12.12' }, + } as React.ChangeEvent); + }); + + instance.update(); + + jest.runAllTimers(); + + expect(updateLayerSpy).not.toHaveBeenCalled(); + + expect( + instance + .find('[data-test-subj="lns-indexPattern-percentile-form"]') + .find(EuiFormRow) + .prop('isInvalid') + ).toEqual(true); + expect( + instance + .find('[data-test-subj="lns-indexPattern-percentile-input"]') + .find(EuiFieldNumber) + .prop('value') + ).toEqual('12.12'); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx new file mode 100644 index 0000000000000..b381a0ecb664a --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx @@ -0,0 +1,189 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFieldNumber, EuiFormRow } from '@elastic/eui'; +import React, { useCallback, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { AggFunctionsMapping } from 'src/plugins/data/public'; +import { buildExpressionFunction } from '../../../../../../../src/plugins/expressions/public'; +import { OperationDefinition } from './index'; +import { + getInvalidFieldMessage, + getSafeName, + isValidNumber, + useDebounceWithOptions, +} from './helpers'; +import { FieldBasedIndexPatternColumn } from './column_types'; + +export interface PercentileIndexPatternColumn extends FieldBasedIndexPatternColumn { + operationType: 'percentile'; + params: { + percentile: number; + format?: { + id: string; + params?: { + decimals: number; + }; + }; + }; +} + +function ofName(name: string, percentile: number) { + return i18n.translate('xpack.lens.indexPattern.percentileOf', { + defaultMessage: + '{percentile, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} percentile of {name}', + values: { name, percentile }, + }); +} + +const DEFAULT_PERCENTILE_VALUE = 95; + +export const percentileOperation: OperationDefinition = { + type: 'percentile', + displayName: i18n.translate('xpack.lens.indexPattern.percentile', { + defaultMessage: 'Percentile', + }), + input: 'field', + getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type: fieldType }) => { + if (fieldType === 'number' && aggregatable && !aggregationRestrictions) { + return { + dataType: 'number', + isBucketed: false, + scale: 'ratio', + }; + } + }, + isTransferable: (column, newIndexPattern) => { + const newField = newIndexPattern.getFieldByName(column.sourceField); + + return Boolean( + newField && + newField.type === 'number' && + newField.aggregatable && + !newField.aggregationRestrictions + ); + }, + getDefaultLabel: (column, indexPattern, columns) => + ofName(getSafeName(column.sourceField, indexPattern), column.params.percentile), + buildColumn: ({ field, previousColumn, indexPattern }) => { + const existingFormat = + previousColumn?.params && 'format' in previousColumn?.params + ? previousColumn?.params?.format + : undefined; + const existingPercentileParam = + previousColumn?.operationType === 'percentile' && previousColumn?.params.percentile; + const newPercentileParam = existingPercentileParam || DEFAULT_PERCENTILE_VALUE; + return { + label: ofName(getSafeName(field.name, indexPattern), newPercentileParam), + dataType: 'number', + operationType: 'percentile', + sourceField: field.name, + isBucketed: false, + scale: 'ratio', + params: { + format: existingFormat, + percentile: newPercentileParam, + }, + }; + }, + onFieldChange: (oldColumn, field) => { + return { + ...oldColumn, + label: ofName(field.displayName, oldColumn.params.percentile), + sourceField: field.name, + }; + }, + toEsAggsFn: (column, columnId, _indexPattern) => { + return buildExpressionFunction('aggPercentiles', { + id: columnId, + enabled: true, + schema: 'metric', + field: column.sourceField, + percents: [column.params.percentile], + }).toAst(); + }, + getEsAggsSuffix: (column) => { + const value = column.params.percentile; + return `.${value}`; + }, + getErrorMessage: (layer, columnId, indexPattern) => + getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), + paramEditor: function PercentileParamEditor({ + layer, + updateLayer, + currentColumn, + columnId, + indexPattern, + }) { + const [inputValue, setInputValue] = useState(String(currentColumn.params.percentile)); + + const inputValueAsNumber = Number(inputValue); + // an input is value if it's not an empty string, parses to a valid number, is between 0 and 100 (exclusive) + // and is an integer + const inputValueIsValid = isValidNumber(inputValue, true, 99, 1); + + useDebounceWithOptions( + () => { + if (!inputValueIsValid) return; + updateLayer({ + ...layer, + columns: { + ...layer.columns, + [columnId]: { + ...currentColumn, + label: currentColumn.customLabel + ? currentColumn.label + : ofName( + indexPattern.getFieldByName(currentColumn.sourceField)?.displayName || + currentColumn.sourceField, + inputValueAsNumber + ), + params: { + ...currentColumn.params, + percentile: inputValueAsNumber, + }, + }, + }, + }); + }, + { skipFirstRender: true }, + 256, + [inputValue] + ); + + const handleInputChange = useCallback((e: React.ChangeEvent) => { + const val = String(e.target.value); + setInputValue(val); + }, []); + return ( + + + + ); + }, +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx index 9ab677bf68f62..420846f7fc801 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx @@ -22,7 +22,7 @@ import { keys, } from '@elastic/eui'; import { IFieldFormat } from '../../../../../../../../src/plugins/data/common'; -import { RangeTypeLens, isValidRange, isValidNumber } from './ranges'; +import { RangeTypeLens, isValidRange } from './ranges'; import { FROM_PLACEHOLDER, TO_PLACEHOLDER, TYPING_DEBOUNCE_TIME } from './constants'; import { NewBucketButton, @@ -30,7 +30,7 @@ import { DraggableBucketContainer, LabelInput, } from '../shared_components'; -import { useDebounceWithOptions } from '../helpers'; +import { isValidNumber, useDebounceWithOptions } from '../helpers'; const generateId = htmlIdGenerator(); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx index c2c52985c6cd2..987c8971aa310 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx @@ -142,7 +142,8 @@ describe('ranges', () => { const esAggsFn = rangeOperation.toEsAggsFn( layer.columns.col1 as RangeIndexPatternColumn, 'col1', - {} as IndexPattern + {} as IndexPattern, + layer ); expect(esAggsFn).toMatchInlineSnapshot(` Object { @@ -184,7 +185,8 @@ describe('ranges', () => { const esAggsFn = rangeOperation.toEsAggsFn( layer.columns.col1 as RangeIndexPatternColumn, 'col1', - {} as IndexPattern + {} as IndexPattern, + layer ); expect(esAggsFn).toEqual( @@ -203,7 +205,8 @@ describe('ranges', () => { const esAggsFn = rangeOperation.toEsAggsFn( layer.columns.col1 as RangeIndexPatternColumn, 'col1', - {} as IndexPattern + {} as IndexPattern, + layer ); expect(esAggsFn).toEqual( @@ -222,7 +225,8 @@ describe('ranges', () => { const esAggsFn = rangeOperation.toEsAggsFn( layer.columns.col1 as RangeIndexPatternColumn, 'col1', - {} as IndexPattern + {} as IndexPattern, + layer ); expect((esAggsFn as { arguments: unknown }).arguments).toEqual( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx index b687e6fe3da50..aa5cc8255a584 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx @@ -19,7 +19,7 @@ import { updateColumnParam } from '../../layer_helpers'; import { supportedFormats } from '../../../format_column'; import { MODES, AUTO_BARS, DEFAULT_INTERVAL, MIN_HISTOGRAM_BARS, SLICES } from './constants'; import { IndexPattern, IndexPatternField } from '../../../types'; -import { getInvalidFieldMessage } from '../helpers'; +import { getInvalidFieldMessage, isValidNumber } from '../helpers'; type RangeType = Omit; // Try to cover all possible serialized states for ranges @@ -52,10 +52,6 @@ export type UpdateParamsFnType = ( value: RangeColumnParams[K] ) => void; -// on initialization values can be null (from the Infinity serialization), so handle it correctly -// or they will be casted to 0 by the editor ( see #78867 ) -export const isValidNumber = (value: number | '' | null): value is number => - value != null && value !== '' && !isNaN(value) && isFinite(value); export const isRangeWithin = (range: RangeType): boolean => range.from <= range.to; const isFullRange = (range: RangeTypeLens): range is FullRangeTypeLens => isValidNumber(range.from) && isValidNumber(range.to); @@ -152,10 +148,10 @@ export const rangeOperation: OperationDefinition = { label: range.label }; // be careful with the fields to set on partial ranges if (isValidNumber(range.from)) { - partialRange.from = range.from; + partialRange.from = Number(range.from); } if (isValidNumber(range.to)) { - partialRange.to = range.to; + partialRange.to = Number(range.to); } return partialRange; }) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index 8462d374c6e6b..625084000fa93 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -23,7 +23,7 @@ import { DataType } from '../../../../types'; import { OperationDefinition } from '../index'; import { FieldBasedIndexPatternColumn } from '../column_types'; import { ValuesRangeInput } from './values_range_input'; -import { getInvalidFieldMessage } from '../helpers'; +import { getEsAggsSuffix, getInvalidFieldMessage } from '../helpers'; import type { IndexPatternLayer } from '../../../types'; function ofName(name?: string) { @@ -119,7 +119,10 @@ export const termsOperation: OperationDefinition { + toEsAggsFn: (column, columnId, _indexPattern, layer) => { return buildExpressionFunction('aggTerms', { id: columnId, enabled: true, schema: 'segment', field: column.sourceField, orderBy: - column.params.orderBy.type === 'alphabetical' ? '_key' : column.params.orderBy.columnId, + column.params.orderBy.type === 'alphabetical' + ? '_key' + : `${column.params.orderBy.columnId}${getEsAggsSuffix( + layer.columns[column.params.orderBy.columnId] + )}`, order: column.params.orderDirection, size: column.params.size, otherBucket: Boolean(column.params.otherBucket), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index 0209c0b9a448b..d60992bda2e2a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -65,7 +65,8 @@ describe('terms', () => { const esAggsFn = termsOperation.toEsAggsFn( { ...termsColumn, params: { ...termsColumn.params, otherBucket: true } }, 'col1', - {} as IndexPattern + {} as IndexPattern, + layer ); expect(esAggsFn).toEqual( expect.objectContaining({ @@ -87,7 +88,8 @@ describe('terms', () => { params: { ...termsColumn.params, otherBucket: false, missingBucket: true }, }, 'col1', - {} as IndexPattern + {} as IndexPattern, + layer ); expect(esAggsFn).toEqual( expect.objectContaining({ @@ -98,6 +100,45 @@ describe('terms', () => { }) ); }); + + it('should include esaggs suffix from other columns in orderby argument', () => { + const termsColumn = layer.columns.col1 as TermsIndexPatternColumn; + const esAggsFn = termsOperation.toEsAggsFn( + { + ...termsColumn, + params: { + ...termsColumn.params, + otherBucket: true, + orderBy: { type: 'column', columnId: 'abcde' }, + }, + }, + 'col1', + {} as IndexPattern, + { + ...layer, + columns: { + ...layer.columns, + abcde: { + dataType: 'number', + isBucketed: false, + operationType: 'percentile', + sourceField: 'abc', + label: '', + params: { + percentile: 12, + }, + }, + }, + } + ); + expect(esAggsFn).toEqual( + expect.objectContaining({ + arguments: expect.objectContaining({ + orderBy: ['abcde.12'], + }), + }) + ); + }); }); describe('onFieldChange', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index 9f2b8eab4e09b..882252132c5b3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -293,6 +293,11 @@ describe('getOperationTypesForField', () => { "operationType": "median", "type": "field", }, + Object { + "field": "bytes", + "operationType": "percentile", + "type": "field", + }, Object { "field": "bytes", "operationType": "last_value", diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts index a5ce4dfbea371..38f51f24aae7d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -20,6 +20,7 @@ import { operationDefinitionMap } from './operations'; import { IndexPattern, IndexPatternPrivateState, IndexPatternLayer } from './types'; import { OriginalColumn } from './rename_columns'; import { dateHistogramOperation } from './operations/definitions'; +import { getEsAggsSuffix } from './operations/definitions/helpers'; function getExpressionForLayer( layer: IndexPatternLayer, @@ -41,15 +42,20 @@ function getExpressionForLayer( expressions.push(...def.toExpression(layer, colId, indexPattern)); } else { aggs.push( - buildExpression({ type: 'expression', chain: [def.toEsAggsFn(col, colId, indexPattern)] }) + buildExpression({ + type: 'expression', + chain: [def.toEsAggsFn(col, colId, indexPattern, layer)], + }) ); } }); const idMap = columnEntries.reduce((currentIdMap, [colId, column], index) => { + const esAggsId = `col-${columnEntries.length === 1 ? 0 : index}-${colId}`; + const suffix = getEsAggsSuffix(column); return { ...currentIdMap, - [`col-${columnEntries.length === 1 ? 0 : index}-${colId}`]: { + [`${esAggsId}${suffix}`]: { ...column, id: colId, }, diff --git a/x-pack/plugins/lens/public/pie_visualization/expression.tsx b/x-pack/plugins/lens/public/pie_visualization/expression.tsx index 5f18ef7c7f637..63261d08ff1a4 100644 --- a/x-pack/plugins/lens/public/pie_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/expression.tsx @@ -140,6 +140,7 @@ export const getPieRenderer = (dependencies: { paletteService={dependencies.paletteService} onClickValue={onClickValue} renderMode={handlers.getRenderMode()} + syncColors={handlers.isSyncColorsEnabled()} /> , domNode, diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx index 458b1a75c4c17..c6eed36f81ab0 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx @@ -71,6 +71,7 @@ describe('PieVisualization component', () => { chartsThemeService, paletteService: chartPluginMock.createPaletteRegistry(), renderMode: 'display' as const, + syncColors: false, }; } @@ -172,6 +173,7 @@ describe('PieVisualization component', () => { { maxDepth: 2, totalSeries: 5, + syncColors: false, behindText: true, }, undefined diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index 56ecf57f2dff7..70a98e4cf8589 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -47,12 +47,13 @@ export function PieComponent( paletteService: PaletteRegistry; onClickValue: (data: LensFilterEvent['data']) => void; renderMode: RenderMode; + syncColors: boolean; } ) { const [firstTable] = Object.values(props.data.tables); const formatters: Record> = {}; - const { chartsThemeService, paletteService, onClickValue } = props; + const { chartsThemeService, paletteService, syncColors, onClickValue } = props; const { shape, groups, @@ -145,6 +146,7 @@ export function PieComponent( behindText: categoryDisplay !== 'hide', maxDepth: bucketColumns.length, totalSeries: totalSeriesCount, + syncColors, }, palette.params ); diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx index 0e2b47410c3f9..97efd39c02fab 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx @@ -18,7 +18,13 @@ import { Fit, } from '@elastic/charts'; import { PaletteOutput } from 'src/plugins/charts/public'; -import { calculateMinInterval, xyChart, XYChart, XYChartProps } from './expression'; +import { + calculateMinInterval, + xyChart, + XYChart, + XYChartProps, + XYChartRenderProps, +} from './expression'; import { LensMultiTable } from '../types'; import { Datatable, DatatableRow } from '../../../../../src/plugins/expressions/public'; import React from 'react'; @@ -382,6 +388,7 @@ describe('xy_expression', () => { describe('XYChart component', () => { let getFormatSpy: jest.Mock; let convertSpy: jest.Mock; + let defaultProps: Omit; const dataWithoutFormats: LensMultiTable = { type: 'lens_multitable', @@ -421,26 +428,25 @@ describe('xy_expression', () => { }; const getRenderedComponent = (data: LensMultiTable, args: XYArgs) => { - return shallow( - - ); + return shallow(); }; beforeEach(() => { convertSpy = jest.fn((x) => x); getFormatSpy = jest.fn(); getFormatSpy.mockReturnValue({ convert: convertSpy }); + + defaultProps = { + formatFactory: getFormatSpy, + timeZone: 'UTC', + renderMode: 'display', + chartsThemeService, + paletteService, + minInterval: 50, + onClickValue, + onSelectRange, + syncColors: false, + }; }); test('it renders line', () => { @@ -448,16 +454,9 @@ describe('xy_expression', () => { const component = shallow( ); expect(component).toMatchSnapshot(); @@ -493,6 +492,7 @@ describe('xy_expression', () => { const component = shallow( { ...args, layers: [{ ...args.layers[0], seriesType: 'line', xScaleType: 'time' }], }} - formatFactory={getFormatSpy} - timeZone="UTC" - renderMode="display" - chartsThemeService={chartsThemeService} - paletteService={paletteService} minInterval={undefined} - onClickValue={onClickValue} - onSelectRange={onSelectRange} /> ); expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` @@ -534,6 +527,7 @@ describe('xy_expression', () => { const component = shallow( { }, }} args={multiLayerArgs} - formatFactory={getFormatSpy} - timeZone="UTC" - renderMode="display" - chartsThemeService={chartsThemeService} - paletteService={paletteService} - minInterval={50} - onClickValue={onClickValue} - onSelectRange={onSelectRange} /> ); @@ -569,6 +555,7 @@ describe('xy_expression', () => { const component = shallow( { ...args, layers: [{ ...args.layers[0], seriesType: 'line', xScaleType: 'linear' }], }} - formatFactory={getFormatSpy} - timeZone="UTC" - renderMode="display" - chartsThemeService={chartsThemeService} - paletteService={paletteService} - minInterval={50} - onClickValue={onClickValue} - onSelectRange={onSelectRange} /> ); expect(component.find(Settings).prop('xDomain')).toBeUndefined(); @@ -597,16 +576,9 @@ describe('xy_expression', () => { const { data, args } = sampleArgs(); const component = shallow( ); expect(component).toMatchSnapshot(); @@ -619,16 +591,9 @@ describe('xy_expression', () => { const { data, args } = sampleArgs(); const component = shallow( ); expect(component).toMatchSnapshot(); @@ -641,16 +606,9 @@ describe('xy_expression', () => { const { data, args } = sampleArgs(); const component = shallow( ); expect(component).toMatchSnapshot(); @@ -666,20 +624,7 @@ describe('xy_expression', () => { // send empty data to the chart data.tables.first.rows = []; - const component = shallow( - - ); + const component = shallow(); expect(component.find(BarSeries)).toHaveLength(0); expect(component.find(EmptyPlaceholder).prop('icon')).toBeDefined(); @@ -690,19 +635,12 @@ describe('xy_expression', () => { const wrapper = mountWithIntl( ); @@ -776,19 +714,12 @@ describe('xy_expression', () => { const wrapper = mountWithIntl( ); @@ -806,18 +737,7 @@ describe('xy_expression', () => { const { args, data } = sampleArgs(); const wrapper = mountWithIntl( - + ); expect(wrapper.find(Settings).first().prop('onBrushEnd')).toBeUndefined(); @@ -837,6 +757,7 @@ describe('xy_expression', () => { const wrapper = mountWithIntl( { }, ], }} - formatFactory={getFormatSpy} - timeZone="UTC" - renderMode="display" - chartsThemeService={chartsThemeService} - paletteService={paletteService} - minInterval={50} - onClickValue={onClickValue} - onSelectRange={onSelectRange} /> ); @@ -892,18 +805,7 @@ describe('xy_expression', () => { const { args, data } = sampleArgs(); const wrapper = mountWithIntl( - + ); expect(wrapper.find(Settings).first().prop('onElementClick')).toBeUndefined(); @@ -913,16 +815,9 @@ describe('xy_expression', () => { const { data, args } = sampleArgs(); const component = shallow( ); expect(component).toMatchSnapshot(); @@ -935,16 +830,9 @@ describe('xy_expression', () => { const { data, args } = sampleArgs(); const component = shallow( ); expect(component).toMatchSnapshot(); @@ -957,19 +845,12 @@ describe('xy_expression', () => { const { data, args } = sampleArgs(); const component = shallow( ); expect(component).toMatchSnapshot(); @@ -984,6 +865,7 @@ describe('xy_expression', () => { const component = shallow( { }, ], }} - formatFactory={getFormatSpy} - timeZone="UTC" - renderMode="display" - chartsThemeService={chartsThemeService} - paletteService={paletteService} - minInterval={50} - onClickValue={onClickValue} - onSelectRange={onSelectRange} /> ); @@ -1014,18 +888,7 @@ describe('xy_expression', () => { test('it passes time zone to the series', () => { const { data, args } = sampleArgs(); const component = shallow( - + ); expect(component.find(LineSeries).at(0).prop('timeZone')).toEqual('CEST'); expect(component.find(LineSeries).at(1).prop('timeZone')).toEqual('CEST'); @@ -1041,18 +904,7 @@ describe('xy_expression', () => { }; delete firstLayer.splitAccessor; const component = shallow( - + ); expect(component.find(BarSeries).at(0).prop('enableHistogramMode')).toEqual(true); }); @@ -1062,18 +914,7 @@ describe('xy_expression', () => { const firstLayer: LayerArgs = { ...args.layers[0], seriesType: 'bar', isHistogram: true }; delete firstLayer.splitAccessor; const component = shallow( - + ); expect(component.find(BarSeries).at(0).prop('enableHistogramMode')).toEqual(false); expect(component.find(BarSeries).at(1).prop('enableHistogramMode')).toEqual(false); @@ -1087,16 +928,9 @@ describe('xy_expression', () => { delete secondLayer.splitAccessor; const component = shallow( ); expect(component.find(LineSeries).at(0).prop('enableHistogramMode')).toEqual(true); @@ -1107,6 +941,7 @@ describe('xy_expression', () => { const { data, args } = sampleArgs(); const component = shallow( { }, ], }} - formatFactory={getFormatSpy} - timeZone="UTC" - renderMode="display" - chartsThemeService={chartsThemeService} - paletteService={paletteService} - minInterval={50} - onClickValue={onClickValue} - onSelectRange={onSelectRange} /> ); expect(component.find(BarSeries).at(0).prop('enableHistogramMode')).toEqual(true); @@ -1136,19 +963,12 @@ describe('xy_expression', () => { const { data, args } = sampleArgs(); const component = shallow( ); expect(component.find(BarSeries).at(0).prop('enableHistogramMode')).toEqual(false); @@ -1541,16 +1361,9 @@ describe('xy_expression', () => { const component = shallow( ); expect(component.find(LineSeries).at(0).prop('xScaleType')).toEqual(ScaleType.Ordinal); @@ -1562,16 +1375,9 @@ describe('xy_expression', () => { const component = shallow( ); expect(component.find(LineSeries).at(0).prop('yScaleType')).toEqual(ScaleType.Sqrt); @@ -1581,20 +1387,7 @@ describe('xy_expression', () => { test('it gets the formatter for the x axis', () => { const { data, args } = sampleArgs(); - shallow( - - ); + shallow(); expect(getFormatSpy).toHaveBeenCalledWith({ id: 'string' }); }); @@ -1604,16 +1397,9 @@ describe('xy_expression', () => { shallow( ); expect(getFormatSpy).toHaveBeenCalledWith({ @@ -1625,20 +1411,7 @@ describe('xy_expression', () => { test('it should pass the formatter function to the axis', () => { const { data, args } = sampleArgs(); - const instance = shallow( - - ); + const instance = shallow(); const tickFormatter = instance.find(Axis).first().prop('tickFormat'); @@ -1661,20 +1434,7 @@ describe('xy_expression', () => { type: 'lens_xy_tickLabelsConfig', }; - const instance = shallow( - - ); + const instance = shallow(); const axisStyle = instance.find(Axis).first().prop('style'); @@ -1695,20 +1455,7 @@ describe('xy_expression', () => { type: 'lens_xy_tickLabelsConfig', }; - const instance = shallow( - - ); + const instance = shallow(); const axisStyle = instance.find(Axis).at(1).prop('style'); @@ -1729,20 +1476,7 @@ describe('xy_expression', () => { type: 'lens_xy_tickLabelsConfig', }; - const instance = shallow( - - ); + const instance = shallow(); const axisStyle = instance.find(Axis).first().prop('style'); @@ -1763,20 +1497,7 @@ describe('xy_expression', () => { type: 'lens_xy_tickLabelsConfig', }; - const instance = shallow( - - ); + const instance = shallow(); const axisStyle = instance.find(Axis).at(1).prop('style'); @@ -1864,20 +1585,7 @@ describe('xy_expression', () => { ], }; - const component = shallow( - - ); + const component = shallow(); const series = component.find(LineSeries); @@ -1939,20 +1647,7 @@ describe('xy_expression', () => { ], }; - const component = shallow( - - ); + const component = shallow(); const series = component.find(LineSeries); @@ -2012,20 +1707,7 @@ describe('xy_expression', () => { ], }; - const component = shallow( - - ); + const component = shallow(); expect(component.find(Settings).prop('showLegend')).toEqual(true); }); @@ -2035,20 +1717,13 @@ describe('xy_expression', () => { const component = shallow( ); @@ -2060,19 +1735,12 @@ describe('xy_expression', () => { const component = shallow( ); @@ -2084,19 +1752,12 @@ describe('xy_expression', () => { const component = shallow( ); @@ -2123,16 +1784,9 @@ describe('xy_expression', () => { const component = shallow( ); @@ -2150,18 +1804,7 @@ describe('xy_expression', () => { args.layers[0].accessors = ['a']; const component = shallow( - + ); expect(component.find(LineSeries).prop('fit')).toEqual({ type: Fit.None }); @@ -2173,18 +1816,7 @@ describe('xy_expression', () => { args.xTitle = 'My custom x-axis title'; const component = shallow( - + ); expect(component.find(Axis).at(0).prop('title')).toEqual('My custom x-axis title'); @@ -2201,18 +1833,7 @@ describe('xy_expression', () => { }; const component = shallow( - + ); const axisStyle = component.find(Axis).first().prop('style'); @@ -2235,18 +1856,7 @@ describe('xy_expression', () => { }; const component = shallow( - + ); expect(component.find(Axis).at(0).prop('gridLine')).toMatchObject({ diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 790416a6c920d..399ba705f2f5e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -76,7 +76,7 @@ export interface XYRender { value: XYChartProps; } -type XYChartRenderProps = XYChartProps & { +export type XYChartRenderProps = XYChartProps & { chartsThemeService: ChartsPluginSetup['theme']; paletteService: PaletteRegistry; formatFactory: FormatFactory; @@ -85,6 +85,7 @@ type XYChartRenderProps = XYChartProps & { onClickValue: (data: LensFilterEvent['data']) => void; onSelectRange: (data: LensBrushEvent['data']) => void; renderMode: RenderMode; + syncColors: boolean; }; export const xyChart: ExpressionFunctionDefinition< @@ -240,6 +241,7 @@ export const getXyChartRenderer = (dependencies: { onClickValue={onClickValue} onSelectRange={onSelectRange} renderMode={handlers.getRenderMode()} + syncColors={handlers.isSyncColorsEnabled()} /> , domNode, @@ -309,6 +311,7 @@ export function XYChart({ onClickValue, onSelectRange, renderMode, + syncColors, }: XYChartRenderProps) { const { legend, layers, fittingFunction, gridlinesVisibilitySettings, valueLabels } = args; const chartTheme = chartsThemeService.useChartsTheme(); @@ -681,6 +684,7 @@ export function XYChart({ maxDepth: 1, behindText: false, totalSeries: colorAssignment.totalSeriesCount, + syncColors, }, palette.params ); diff --git a/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.mock.ts new file mode 100644 index 0000000000000..4e6655ec1d1d6 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.mock.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 { ID, LIST_ID, NAMESPACE_TYPE } from '../../constants.mock'; + +import { ExportExceptionListQuerySchema } from './export_exception_list_query_schema'; + +export const getExportExceptionListQuerySchemaMock = (): ExportExceptionListQuerySchema => ({ + id: ID, + list_id: LIST_ID, + namespace_type: NAMESPACE_TYPE, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.test.ts new file mode 100644 index 0000000000000..6af7f6323c135 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.test.ts @@ -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 { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; + +import { + ExportExceptionListQuerySchema, + exportExceptionListQuerySchema, +} from './export_exception_list_query_schema'; +import { getExportExceptionListQuerySchemaMock } from './export_exception_list_query_schema.mock'; + +describe('export_exception_list_schema', () => { + test('it should validate a typical lists request', () => { + const payload = getExportExceptionListQuerySchemaMock(); + const decoded = exportExceptionListQuerySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT accept an undefined for an id', () => { + const payload = getExportExceptionListQuerySchemaMock(); + // @ts-expect-error + delete payload.id; + const decoded = exportExceptionListQuerySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "undefined" supplied to "id"']); + expect(message.schema).toEqual({}); + }); + + test('it should default namespace_type to "single" if an undefined given for namespacetype', () => { + const payload = getExportExceptionListQuerySchemaMock(); + delete payload.namespace_type; + const decoded = exportExceptionListQuerySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(message.schema).toEqual({ + id: 'uuid_here', + list_id: 'some-list-id', + namespace_type: 'single', + }); + }); + + test('it should NOT accept an undefined for an list_id', () => { + const payload = getExportExceptionListQuerySchemaMock(); + // @ts-expect-error + delete payload.list_id; + const decoded = exportExceptionListQuerySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "list_id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: ExportExceptionListQuerySchema & { + extraKey?: string; + } = getExportExceptionListQuerySchemaMock(); + payload.extraKey = 'some new value'; + const decoded = exportExceptionListQuerySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.ts b/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.ts new file mode 100644 index 0000000000000..b5061e903a824 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.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 * as t from 'io-ts'; + +import { id, list_id, namespace_type } from '../common/schemas'; + +export const exportExceptionListQuerySchema = t.exact( + t.type({ + id, + list_id, + namespace_type, + // TODO: Add file_name here with a default value + }) +); + +export type ExportExceptionListQuerySchema = t.OutputOf; diff --git a/x-pack/plugins/lists/common/schemas/request/index.ts b/x-pack/plugins/lists/common/schemas/request/index.ts index 172d73a5c7377..36e41bf52aa40 100644 --- a/x-pack/plugins/lists/common/schemas/request/index.ts +++ b/x-pack/plugins/lists/common/schemas/request/index.ts @@ -14,6 +14,7 @@ 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_exception_list_query_schema'; export * from './export_list_item_query_schema'; export * from './find_endpoint_list_item_schema'; export * from './find_exception_list_item_schema'; diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts index e45403e319c29..96d5fbf010f42 100644 --- a/x-pack/plugins/lists/public/exceptions/api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/api.test.ts @@ -11,12 +11,6 @@ import { getCreateExceptionListItemSchemaMock } from '../../common/schemas/reque import { getFoundExceptionListItemSchemaMock } from '../../common/schemas/response/found_exception_list_item_schema.mock'; import { getUpdateExceptionListItemSchemaMock } from '../../common/schemas/request/update_exception_list_item_schema.mock'; import { getUpdateExceptionListSchemaMock } from '../../common/schemas/request/update_exception_list_schema.mock'; -import { - CreateExceptionListItemSchema, - CreateExceptionListSchema, - ExceptionListItemSchema, - ExceptionListSchema, -} from '../../common/schemas'; import { getFoundExceptionListSchemaMock } from '../../common/schemas/response/found_exception_list_schema.mock'; import { @@ -25,6 +19,7 @@ import { addExceptionListItem, deleteExceptionListById, deleteExceptionListItemById, + exportExceptionList, fetchExceptionListById, fetchExceptionListItemById, fetchExceptionLists, @@ -32,7 +27,6 @@ import { updateExceptionList, updateExceptionListItem, } from './api'; -import { ApiCallByIdProps, ApiCallByListIdProps, ApiCallFetchExceptionListsProps } from './types'; const abortCtrl = new AbortController(); @@ -74,20 +68,6 @@ describe('Exceptions Lists API', () => { expect(exceptionResponse).toEqual(getExceptionListSchemaMock()); }); - test('it returns error and does not make request if request payload fails decode', async () => { - const payload: Omit & { - description?: string[]; - } = { ...getCreateExceptionListSchemaMock(), description: ['123'] }; - - await expect( - addExceptionList({ - http: httpMock, - list: (payload as unknown) as ExceptionListSchema, - signal: abortCtrl.signal, - }) - ).rejects.toEqual('Invalid value "["123"]" supplied to "description"'); - }); - test('it returns error if response payload fails decode', async () => { const payload = getCreateExceptionListSchemaMock(); const badPayload = getExceptionListSchemaMock(); @@ -101,7 +81,7 @@ describe('Exceptions Lists API', () => { list: payload, signal: abortCtrl.signal, }) - ).rejects.toEqual('Invalid value "undefined" supplied to "id"'); + ).rejects.toEqual(new Error('Invalid value "undefined" supplied to "id"')); }); }); @@ -136,20 +116,6 @@ describe('Exceptions Lists API', () => { expect(exceptionResponse).toEqual(getExceptionListItemSchemaMock()); }); - test('it returns error and does not make request if request payload fails decode', async () => { - const payload: Omit & { - description?: string[]; - } = { ...getCreateExceptionListItemSchemaMock(), description: ['123'] }; - - await expect( - addExceptionListItem({ - http: httpMock, - listItem: (payload as unknown) as ExceptionListItemSchema, - signal: abortCtrl.signal, - }) - ).rejects.toEqual('Invalid value "["123"]" supplied to "description"'); - }); - test('it returns error if response payload fails decode', async () => { const payload = getCreateExceptionListItemSchemaMock(); const badPayload = getExceptionListItemSchemaMock(); @@ -163,7 +129,7 @@ describe('Exceptions Lists API', () => { listItem: payload, signal: abortCtrl.signal, }) - ).rejects.toEqual('Invalid value "undefined" supplied to "id"'); + ).rejects.toEqual(new Error('Invalid value "undefined" supplied to "id"')); }); }); @@ -198,20 +164,6 @@ describe('Exceptions Lists API', () => { expect(exceptionResponse).toEqual(getExceptionListSchemaMock()); }); - test('it returns error and does not make request if request payload fails decode', async () => { - const payload = getUpdateExceptionListSchemaMock(); - // @ts-expect-error - delete payload.description; - - await expect( - updateExceptionList({ - http: httpMock, - list: payload, - signal: abortCtrl.signal, - }) - ).rejects.toEqual('Invalid value "undefined" supplied to "description"'); - }); - test('it returns error if response payload fails decode', async () => { const payload = getUpdateExceptionListSchemaMock(); const badPayload = getExceptionListSchemaMock(); @@ -225,7 +177,7 @@ describe('Exceptions Lists API', () => { list: payload, signal: abortCtrl.signal, }) - ).rejects.toEqual('Invalid value "undefined" supplied to "id"'); + ).rejects.toEqual(new Error('Invalid value "undefined" supplied to "id"')); }); }); @@ -260,20 +212,6 @@ describe('Exceptions Lists API', () => { expect(exceptionResponse).toEqual(getExceptionListItemSchemaMock()); }); - test('it returns error and does not make request if request payload fails decode', async () => { - const payload = getUpdateExceptionListItemSchemaMock(); - // @ts-expect-error - delete payload.description; - - await expect( - updateExceptionListItem({ - http: httpMock, - listItem: payload, - signal: abortCtrl.signal, - }) - ).rejects.toEqual('Invalid value "undefined" supplied to "description"'); - }); - test('it returns error if response payload fails decode', async () => { const payload = getUpdateExceptionListItemSchemaMock(); const badPayload = getExceptionListItemSchemaMock(); @@ -287,7 +225,7 @@ describe('Exceptions Lists API', () => { listItem: payload, signal: abortCtrl.signal, }) - ).rejects.toEqual('Invalid value "undefined" supplied to "id"'); + ).rejects.toEqual(new Error('Invalid value "undefined" supplied to "id"')); }); }); @@ -335,22 +273,6 @@ describe('Exceptions Lists API', () => { expect(exceptionResponse.data).toEqual([getExceptionListSchemaMock()]); }); - test('it returns error and does not make request if request payload fails decode', async () => { - const payload = ({ - filters: 'exception-list.attributes.name: Sample Endpoint', - http: httpMock, - namespaceTypes: 'notANamespaceType', - pagination: { - page: 1, - perPage: 20, - }, - signal: abortCtrl.signal, - } as unknown) as ApiCallFetchExceptionListsProps & { namespaceTypes: string[] }; - await expect(fetchExceptionLists(payload)).rejects.toEqual( - 'Invalid value "notANamespaceType" supplied to "namespace_type"' - ); - }); - test('it returns error if response payload fails decode', async () => { const badPayload = getExceptionListSchemaMock(); // @ts-expect-error @@ -368,7 +290,7 @@ describe('Exceptions Lists API', () => { }, signal: abortCtrl.signal, }) - ).rejects.toEqual('Invalid value "undefined" supplied to "data,id"'); + ).rejects.toEqual(new Error('Invalid value "undefined" supplied to "data,id"')); }); }); @@ -404,18 +326,6 @@ describe('Exceptions Lists API', () => { expect(exceptionResponse).toEqual(getExceptionListSchemaMock()); }); - test('it returns error and does not make request if request payload fails decode', async () => { - const payload = ({ - http: httpMock, - id: 1, - namespaceType: 'single', - signal: abortCtrl.signal, - } as unknown) as ApiCallByIdProps & { id: number }; - await expect(fetchExceptionListById(payload)).rejects.toEqual( - 'Invalid value "1" supplied to "id"' - ); - }); - test('it returns error if response payload fails decode', async () => { const badPayload = getExceptionListSchemaMock(); // @ts-expect-error @@ -429,7 +339,7 @@ describe('Exceptions Lists API', () => { namespaceType: 'single', signal: abortCtrl.signal, }) - ).rejects.toEqual('Invalid value "undefined" supplied to "id"'); + ).rejects.toEqual(new Error('Invalid value "undefined" supplied to "id"')); }); }); @@ -613,23 +523,6 @@ describe('Exceptions Lists API', () => { expect(exceptionResponse).toEqual(getFoundExceptionListItemSchemaMock()); }); - test('it returns error and does not make request if request payload fails decode', async () => { - const payload = ({ - filterOptions: [], - http: httpMock, - listIds: ['myList'], - namespaceTypes: ['not a namespace type'], - pagination: { - page: 1, - perPage: 20, - }, - signal: abortCtrl.signal, - } as unknown) as ApiCallByListIdProps & { listId: number }; - await expect(fetchExceptionListsItemsByListIds(payload)).rejects.toEqual( - 'Invalid value "not a namespace type" supplied to "namespace_type"' - ); - }); - test('it returns error if response payload fails decode', async () => { const badPayload = getExceptionListItemSchemaMock(); // @ts-expect-error @@ -649,7 +542,9 @@ describe('Exceptions Lists API', () => { signal: abortCtrl.signal, }) ).rejects.toEqual( - 'Invalid value "undefined" supplied to "data",Invalid value "undefined" supplied to "page",Invalid value "undefined" supplied to "per_page",Invalid value "undefined" supplied to "total"' + new Error( + 'Invalid value "undefined" supplied to "data",Invalid value "undefined" supplied to "page",Invalid value "undefined" supplied to "per_page",Invalid value "undefined" supplied to "total"' + ) ); }); }); @@ -686,18 +581,6 @@ describe('Exceptions Lists API', () => { expect(exceptionResponse).toEqual(getExceptionListItemSchemaMock()); }); - test('it returns error and does not make request if request payload fails decode', async () => { - const payload = ({ - http: httpMock, - id: '1', - namespaceType: 'not a namespace type', - signal: abortCtrl.signal, - } as unknown) as ApiCallByIdProps & { namespaceType: string }; - await expect(fetchExceptionListItemById(payload)).rejects.toEqual( - 'Invalid value "not a namespace type" supplied to "namespace_type"' - ); - }); - test('it returns error if response payload fails decode', async () => { const badPayload = getExceptionListItemSchemaMock(); // @ts-expect-error @@ -711,7 +594,7 @@ describe('Exceptions Lists API', () => { namespaceType: 'single', signal: abortCtrl.signal, }) - ).rejects.toEqual('Invalid value "undefined" supplied to "id"'); + ).rejects.toEqual(new Error('Invalid value "undefined" supplied to "id"')); }); }); @@ -747,18 +630,6 @@ describe('Exceptions Lists API', () => { expect(exceptionResponse).toEqual(getExceptionListSchemaMock()); }); - test('it returns error and does not make request if request payload fails decode', async () => { - const payload = ({ - http: httpMock, - id: 1, - namespaceType: 'single', - signal: abortCtrl.signal, - } as unknown) as ApiCallByIdProps & { id: number }; - await expect(deleteExceptionListById(payload)).rejects.toEqual( - 'Invalid value "1" supplied to "id"' - ); - }); - test('it returns error if response payload fails decode', async () => { const badPayload = getExceptionListSchemaMock(); // @ts-expect-error @@ -772,7 +643,7 @@ describe('Exceptions Lists API', () => { namespaceType: 'single', signal: abortCtrl.signal, }) - ).rejects.toEqual('Invalid value "undefined" supplied to "id"'); + ).rejects.toEqual(new Error('Invalid value "undefined" supplied to "id"')); }); }); @@ -808,18 +679,6 @@ describe('Exceptions Lists API', () => { expect(exceptionResponse).toEqual(getExceptionListItemSchemaMock()); }); - test('it returns error and does not make request if request payload fails decode', async () => { - const payload = ({ - http: httpMock, - id: 1, - namespaceType: 'single', - signal: abortCtrl.signal, - } as unknown) as ApiCallByIdProps & { id: number }; - await expect(deleteExceptionListItemById(payload)).rejects.toEqual( - 'Invalid value "1" supplied to "id"' - ); - }); - test('it returns error if response payload fails decode', async () => { const badPayload = getExceptionListItemSchemaMock(); // @ts-expect-error @@ -833,7 +692,7 @@ describe('Exceptions Lists API', () => { namespaceType: 'single', signal: abortCtrl.signal, }) - ).rejects.toEqual('Invalid value "undefined" supplied to "id"'); + ).rejects.toEqual(new Error('Invalid value "undefined" supplied to "id"')); }); }); @@ -870,4 +729,50 @@ describe('Exceptions Lists API', () => { expect(exceptionResponse).toEqual({}); }); }); + + describe('#exportExceptionList', () => { + const blob: Blob = { + arrayBuffer: jest.fn(), + size: 89, + slice: jest.fn(), + stream: jest.fn(), + text: jest.fn(), + type: 'json', + } as Blob; + + beforeEach(() => { + httpMock.fetch.mockResolvedValue(blob); + }); + + test('it invokes "exportExceptionList" with expected url and body values', async () => { + await exportExceptionList({ + http: httpMock, + id: 'some-id', + listId: 'list-id', + namespaceType: 'single', + signal: abortCtrl.signal, + }); + + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/_export', { + method: 'GET', + query: { + id: 'some-id', + list_id: 'list-id', + namespace_type: 'single', + }, + signal: abortCtrl.signal, + }); + }); + + test('it returns expected list to export on success', async () => { + const exceptionResponse = await exportExceptionList({ + http: httpMock, + id: 'some-id', + listId: 'list-id', + namespaceType: 'single', + signal: abortCtrl.signal, + }); + expect(exceptionResponse).toEqual(blob); + }); + }); }); diff --git a/x-pack/plugins/lists/public/exceptions/api.ts b/x-pack/plugins/lists/public/exceptions/api.ts index fc0c8934d6397..8fcd1af524f6d 100644 --- a/x-pack/plugins/lists/public/exceptions/api.ts +++ b/x-pack/plugins/lists/public/exceptions/api.ts @@ -3,6 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { chain, fromEither, tryCatch } from 'fp-ts/lib/TaskEither'; +import { flow } from 'fp-ts/lib/function'; + +import { validateEither } from '../../common/shared_imports'; +import { toError, toPromise } from '../common/fp_utils'; import { ENDPOINT_LIST_URL, EXCEPTION_LIST_ITEM_URL, @@ -17,22 +22,11 @@ import { FoundExceptionListItemSchema, FoundExceptionListSchema, createEndpointListSchema, - createExceptionListItemSchema, - createExceptionListSchema, - deleteExceptionListItemSchema, - deleteExceptionListSchema, exceptionListItemSchema, exceptionListSchema, - findExceptionListItemSchema, - findExceptionListSchema, foundExceptionListItemSchema, foundExceptionListSchema, - readExceptionListItemSchema, - readExceptionListSchema, - updateExceptionListItemSchema, - updateExceptionListSchema, } from '../../common/schemas'; -import { validate } from '../../common/shared_imports'; import { AddEndpointExceptionListProps, @@ -41,6 +35,7 @@ import { ApiCallByIdProps, ApiCallByListIdProps, ApiCallFetchExceptionListsProps, + ExportExceptionListProps, UpdateExceptionListItemProps, UpdateExceptionListProps, } from './types'; @@ -55,35 +50,38 @@ import { * @throws An error if response is not OK * */ -export const addExceptionList = async ({ +const addExceptionList = async ({ http, list, signal, -}: AddExceptionListProps): Promise => { - const [validatedRequest, errorsRequest] = validate(list, createExceptionListSchema); - - if (validatedRequest != null) { - try { - const response = await http.fetch(EXCEPTION_LIST_URL, { - body: JSON.stringify(list), - method: 'POST', - signal, - }); - - const [validatedResponse, errorsResponse] = validate(response, exceptionListSchema); - - if (errorsResponse != null || validatedResponse == null) { - return Promise.reject(errorsResponse); - } else { - return Promise.resolve(validatedResponse); - } - } catch (error) { - return Promise.reject(error); - } - } else { - return Promise.reject(errorsRequest); - } -}; +}: AddExceptionListProps): Promise => + http.fetch(EXCEPTION_LIST_URL, { + body: JSON.stringify(list), + method: 'POST', + signal, + }); + +const addExceptionListWithValidation = async ({ + http, + list, + signal, +}: AddExceptionListProps): Promise => + flow( + () => + tryCatch( + () => + addExceptionList({ + http, + list, + signal, + }), + toError + ), + chain((response) => fromEither(validateEither(exceptionListSchema, response))), + flow(toPromise) + )(); + +export { addExceptionListWithValidation as addExceptionList }; /** * Add new ExceptionListItem @@ -95,35 +93,38 @@ export const addExceptionList = async ({ * @throws An error if response is not OK * */ -export const addExceptionListItem = async ({ +const addExceptionListItem = async ({ http, listItem, signal, -}: AddExceptionListItemProps): Promise => { - const [validatedRequest, errorsRequest] = validate(listItem, createExceptionListItemSchema); - - if (validatedRequest != null) { - try { - const response = await http.fetch(EXCEPTION_LIST_ITEM_URL, { - body: JSON.stringify(listItem), - method: 'POST', - signal, - }); - - const [validatedResponse, errorsResponse] = validate(response, exceptionListItemSchema); - - if (errorsResponse != null || validatedResponse == null) { - return Promise.reject(errorsResponse); - } else { - return Promise.resolve(validatedResponse); - } - } catch (error) { - return Promise.reject(error); - } - } else { - return Promise.reject(errorsRequest); - } -}; +}: AddExceptionListItemProps): Promise => + http.fetch(EXCEPTION_LIST_ITEM_URL, { + body: JSON.stringify(listItem), + method: 'POST', + signal, + }); + +const addExceptionListItemWithValidation = async ({ + http, + listItem, + signal, +}: AddExceptionListItemProps): Promise => + flow( + () => + tryCatch( + () => + addExceptionListItem({ + http, + listItem, + signal, + }), + toError + ), + chain((response) => fromEither(validateEither(exceptionListItemSchema, response))), + flow(toPromise) + )(); + +export { addExceptionListItemWithValidation as addExceptionListItem }; /** * Update existing ExceptionList @@ -135,35 +136,38 @@ export const addExceptionListItem = async ({ * @throws An error if response is not OK * */ -export const updateExceptionList = async ({ +const updateExceptionList = async ({ http, list, signal, -}: UpdateExceptionListProps): Promise => { - const [validatedRequest, errorsRequest] = validate(list, updateExceptionListSchema); - - if (validatedRequest != null) { - try { - const response = await http.fetch(EXCEPTION_LIST_URL, { - body: JSON.stringify(list), - method: 'PUT', - signal, - }); - - const [validatedResponse, errorsResponse] = validate(response, exceptionListSchema); - - if (errorsResponse != null || validatedResponse == null) { - return Promise.reject(errorsResponse); - } else { - return Promise.resolve(validatedResponse); - } - } catch (error) { - return Promise.reject(error); - } - } else { - return Promise.reject(errorsRequest); - } -}; +}: UpdateExceptionListProps): Promise => + http.fetch(EXCEPTION_LIST_URL, { + body: JSON.stringify(list), + method: 'PUT', + signal, + }); + +const updateExceptionListWithValidation = async ({ + http, + list, + signal, +}: UpdateExceptionListProps): Promise => + flow( + () => + tryCatch( + () => + updateExceptionList({ + http, + list, + signal, + }), + toError + ), + chain((response) => fromEither(validateEither(exceptionListSchema, response))), + flow(toPromise) + )(); + +export { updateExceptionListWithValidation as updateExceptionList }; /** * Update existing ExceptionListItem @@ -175,35 +179,38 @@ export const updateExceptionList = async ({ * @throws An error if response is not OK * */ -export const updateExceptionListItem = async ({ +const updateExceptionListItem = async ({ http, listItem, signal, -}: UpdateExceptionListItemProps): Promise => { - const [validatedRequest, errorsRequest] = validate(listItem, updateExceptionListItemSchema); - - if (validatedRequest != null) { - try { - const response = await http.fetch(EXCEPTION_LIST_ITEM_URL, { - body: JSON.stringify(listItem), - method: 'PUT', - signal, - }); - - const [validatedResponse, errorsResponse] = validate(response, exceptionListItemSchema); - - if (errorsResponse != null || validatedResponse == null) { - return Promise.reject(errorsResponse); - } else { - return Promise.resolve(validatedResponse); - } - } catch (error) { - return Promise.reject(error); - } - } else { - return Promise.reject(errorsRequest); - } -}; +}: UpdateExceptionListItemProps): Promise => + http.fetch(EXCEPTION_LIST_ITEM_URL, { + body: JSON.stringify(listItem), + method: 'PUT', + signal, + }); + +const updateExceptionListItemWithValidation = async ({ + http, + listItem, + signal, +}: UpdateExceptionListItemProps): Promise => + flow( + () => + tryCatch( + () => + updateExceptionListItem({ + http, + listItem, + signal, + }), + toError + ), + chain((response) => fromEither(validateEither(exceptionListItemSchema, response))), + flow(toPromise) + )(); + +export { updateExceptionListItemWithValidation as updateExceptionListItem }; /** * Fetch all ExceptionLists (optionally by namespaceType) @@ -216,7 +223,7 @@ export const updateExceptionListItem = async ({ * * @throws An error if request params or response is not OK */ -export const fetchExceptionLists = async ({ +const fetchExceptionLists = async ({ http, filters, namespaceTypes, @@ -232,31 +239,39 @@ export const fetchExceptionLists = async ({ sort_order: 'desc', }; - const [validatedRequest, errorsRequest] = validate(query, findExceptionListSchema); - - if (validatedRequest != null) { - try { - const response = await http.fetch(`${EXCEPTION_LIST_URL}/_find`, { - method: 'GET', - query, - signal, - }); - - const [validatedResponse, errorsResponse] = validate(response, foundExceptionListSchema); - - if (errorsResponse != null || validatedResponse == null) { - return Promise.reject(errorsResponse); - } else { - return Promise.resolve(validatedResponse); - } - } catch (error) { - return Promise.reject(error); - } - } else { - return Promise.reject(errorsRequest); - } + return http.fetch(`${EXCEPTION_LIST_URL}/_find`, { + method: 'GET', + query, + signal, + }); }; +const fetchExceptionListsWithValidation = async ({ + filters, + http, + namespaceTypes, + pagination, + signal, +}: ApiCallFetchExceptionListsProps): Promise => + flow( + () => + tryCatch( + () => + fetchExceptionLists({ + filters, + http, + namespaceTypes, + pagination, + signal, + }), + toError + ), + chain((response) => fromEither(validateEither(foundExceptionListSchema, response))), + flow(toPromise) + )(); + +export { fetchExceptionListsWithValidation as fetchExceptionLists }; + /** * Fetch an ExceptionList by providing a ExceptionList ID * @@ -267,39 +282,41 @@ export const fetchExceptionLists = async ({ * * @throws An error if response is not OK */ -export const fetchExceptionListById = async ({ +const fetchExceptionListById = async ({ http, id, namespaceType, signal, -}: ApiCallByIdProps): Promise => { - const [validatedRequest, errorsRequest] = validate( - { id, namespace_type: namespaceType }, - readExceptionListSchema - ); - - if (validatedRequest != null) { - try { - const response = await http.fetch(EXCEPTION_LIST_URL, { - method: 'GET', - query: { id, namespace_type: namespaceType }, - signal, - }); - - const [validatedResponse, errorsResponse] = validate(response, exceptionListSchema); - - if (errorsResponse != null || validatedResponse == null) { - return Promise.reject(errorsResponse); - } else { - return Promise.resolve(validatedResponse); - } - } catch (error) { - return Promise.reject(error); - } - } else { - return Promise.reject(errorsRequest); - } -}; +}: ApiCallByIdProps): Promise => + http.fetch(EXCEPTION_LIST_URL, { + method: 'GET', + query: { id, namespace_type: namespaceType }, + signal, + }); + +const fetchExceptionListByIdWithValidation = async ({ + http, + id, + namespaceType, + signal, +}: ApiCallByIdProps): Promise => + flow( + () => + tryCatch( + () => + fetchExceptionListById({ + http, + id, + namespaceType, + signal, + }), + toError + ), + chain((response) => fromEither(validateEither(exceptionListSchema, response))), + flow(toPromise) + )(); + +export { fetchExceptionListByIdWithValidation as fetchExceptionListById }; /** * Fetch an ExceptionList's ExceptionItems by providing a ExceptionList list_id @@ -313,7 +330,7 @@ export const fetchExceptionListById = async ({ * * @throws An error if response is not OK */ -export const fetchExceptionListsItemsByListIds = async ({ +const fetchExceptionListsItemsByListIds = async ({ http, listIds, namespaceTypes, @@ -348,34 +365,42 @@ export const fetchExceptionListsItemsByListIds = async ({ sort_order: 'desc', ...(filters.trim() !== '' ? { filter: filters } : {}), }; - const [validatedRequest, errorsRequest] = validate(query, findExceptionListItemSchema); - - if (validatedRequest != null) { - try { - const response = await http.fetch( - `${EXCEPTION_LIST_ITEM_URL}/_find`, - { - method: 'GET', - query, - signal, - } - ); - - const [validatedResponse, errorsResponse] = validate(response, foundExceptionListItemSchema); - - if (errorsResponse != null || validatedResponse == null) { - return Promise.reject(errorsResponse); - } else { - return Promise.resolve(validatedResponse); - } - } catch (error) { - return Promise.reject(error); - } - } else { - return Promise.reject(errorsRequest); - } + + return http.fetch(`${EXCEPTION_LIST_ITEM_URL}/_find`, { + method: 'GET', + query, + signal, + }); }; +const fetchExceptionListsItemsByListIdsWithValidation = async ({ + filterOptions, + http, + listIds, + namespaceTypes, + pagination, + signal, +}: ApiCallByListIdProps): Promise => + flow( + () => + tryCatch( + () => + fetchExceptionListsItemsByListIds({ + filterOptions, + http, + listIds, + namespaceTypes, + pagination, + signal, + }), + toError + ), + chain((response) => fromEither(validateEither(foundExceptionListItemSchema, response))), + flow(toPromise) + )(); + +export { fetchExceptionListsItemsByListIdsWithValidation as fetchExceptionListsItemsByListIds }; + /** * Fetch an ExceptionListItem by providing a ExceptionListItem ID * @@ -386,38 +411,31 @@ export const fetchExceptionListsItemsByListIds = async ({ * * @throws An error if response is not OK */ -export const fetchExceptionListItemById = async ({ +const fetchExceptionListItemById = async ({ http, id, namespaceType, signal, -}: ApiCallByIdProps): Promise => { - const [validatedRequest, errorsRequest] = validate( - { id, namespace_type: namespaceType }, - readExceptionListItemSchema - ); - - if (validatedRequest != null) { - try { - const response = await http.fetch(EXCEPTION_LIST_ITEM_URL, { - method: 'GET', - query: { id, namespace_type: namespaceType }, - signal, - }); - const [validatedResponse, errorsResponse] = validate(response, exceptionListItemSchema); - - if (errorsResponse != null || validatedResponse == null) { - return Promise.reject(errorsResponse); - } else { - return Promise.resolve(validatedResponse); - } - } catch (error) { - return Promise.reject(error); - } - } else { - return Promise.reject(errorsRequest); - } -}; +}: ApiCallByIdProps): Promise => + http.fetch(EXCEPTION_LIST_ITEM_URL, { + method: 'GET', + query: { id, namespace_type: namespaceType }, + signal, + }); + +const fetchExceptionListItemByIdWithValidation = async ({ + http, + id, + namespaceType, + signal, +}: ApiCallByIdProps): Promise => + flow( + () => tryCatch(() => fetchExceptionListItemById({ http, id, namespaceType, signal }), toError), + chain((response) => fromEither(validateEither(exceptionListItemSchema, response))), + flow(toPromise) + )(); + +export { fetchExceptionListItemByIdWithValidation as fetchExceptionListItemById }; /** * Delete an ExceptionList by providing a ExceptionList ID @@ -429,39 +447,31 @@ export const fetchExceptionListItemById = async ({ * * @throws An error if response is not OK */ -export const deleteExceptionListById = async ({ +const deleteExceptionListById = async ({ http, id, namespaceType, signal, -}: ApiCallByIdProps): Promise => { - const [validatedRequest, errorsRequest] = validate( - { id, namespace_type: namespaceType }, - deleteExceptionListSchema - ); - - if (validatedRequest != null) { - try { - const response = await http.fetch(EXCEPTION_LIST_URL, { - method: 'DELETE', - query: { id, namespace_type: namespaceType }, - signal, - }); - - const [validatedResponse, errorsResponse] = validate(response, exceptionListSchema); - - if (errorsResponse != null || validatedResponse == null) { - return Promise.reject(errorsResponse); - } else { - return Promise.resolve(validatedResponse); - } - } catch (error) { - return Promise.reject(error); - } - } else { - return Promise.reject(errorsRequest); - } -}; +}: ApiCallByIdProps): Promise => + http.fetch(EXCEPTION_LIST_URL, { + method: 'DELETE', + query: { id, namespace_type: namespaceType }, + signal, + }); + +const deleteExceptionListByIdWithValidation = async ({ + http, + id, + namespaceType, + signal, +}: ApiCallByIdProps): Promise => + flow( + () => tryCatch(() => deleteExceptionListById({ http, id, namespaceType, signal }), toError), + chain((response) => fromEither(validateEither(exceptionListSchema, response))), + flow(toPromise) + )(); + +export { deleteExceptionListByIdWithValidation as deleteExceptionListById }; /** * Delete an ExceptionListItem by providing a ExceptionListItem ID @@ -473,39 +483,31 @@ export const deleteExceptionListById = async ({ * * @throws An error if response is not OK */ -export const deleteExceptionListItemById = async ({ +const deleteExceptionListItemById = async ({ http, id, namespaceType, signal, -}: ApiCallByIdProps): Promise => { - const [validatedRequest, errorsRequest] = validate( - { id, namespace_type: namespaceType }, - deleteExceptionListItemSchema - ); - - if (validatedRequest != null) { - try { - const response = await http.fetch(EXCEPTION_LIST_ITEM_URL, { - method: 'DELETE', - query: { id, namespace_type: namespaceType }, - signal, - }); - - const [validatedResponse, errorsResponse] = validate(response, exceptionListItemSchema); - - if (errorsResponse != null || validatedResponse == null) { - return Promise.reject(errorsResponse); - } else { - return Promise.resolve(validatedResponse); - } - } catch (error) { - return Promise.reject(error); - } - } else { - return Promise.reject(errorsRequest); - } -}; +}: ApiCallByIdProps): Promise => + http.fetch(EXCEPTION_LIST_ITEM_URL, { + method: 'DELETE', + query: { id, namespace_type: namespaceType }, + signal, + }); + +const deleteExceptionListItemByIdWithValidation = async ({ + http, + id, + namespaceType, + signal, +}: ApiCallByIdProps): Promise => + flow( + () => tryCatch(() => deleteExceptionListItemById({ http, id, namespaceType, signal }), toError), + chain((response) => fromEither(validateEither(exceptionListItemSchema, response))), + flow(toPromise) + )(); + +export { deleteExceptionListItemByIdWithValidation as deleteExceptionListItemById }; /** * Add new Endpoint ExceptionList @@ -516,24 +518,47 @@ export const deleteExceptionListItemById = async ({ * @throws An error if response is not OK * */ -export const addEndpointExceptionList = async ({ +const addEndpointExceptionList = async ({ http, signal, -}: AddEndpointExceptionListProps): Promise => { - try { - const response = await http.fetch(ENDPOINT_LIST_URL, { - method: 'POST', - signal, - }); - - const [validatedResponse, errorsResponse] = validate(response, createEndpointListSchema); - - if (errorsResponse != null || validatedResponse == null) { - return Promise.reject(errorsResponse); - } else { - return Promise.resolve(validatedResponse); - } - } catch (error) { - return Promise.reject(error); - } -}; +}: AddEndpointExceptionListProps): Promise => + http.fetch(ENDPOINT_LIST_URL, { + method: 'POST', + signal, + }); + +const addEndpointExceptionListWithValidation = async ({ + http, + signal, +}: AddEndpointExceptionListProps): Promise => + flow( + () => tryCatch(() => addEndpointExceptionList({ http, signal }), toError), + chain((response) => fromEither(validateEither(createEndpointListSchema, response))), + flow(toPromise) + )(); + +export { addEndpointExceptionListWithValidation as addEndpointExceptionList }; + +/** + * Fetch an ExceptionList by providing a ExceptionList ID + * + * @param http Kibana http service + * @param id ExceptionList ID (not list_id) + * @param listId ExceptionList LIST_ID (not id) + * @param namespaceType ExceptionList namespace_type + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const exportExceptionList = async ({ + http, + id, + listId, + namespaceType, + signal, +}: ExportExceptionListProps): Promise => + http.fetch(`${EXCEPTION_LIST_URL}/_export`, { + method: 'GET', + query: { id, list_id: listId, namespace_type: namespaceType }, + signal, + }); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_api.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_api.ts index def2f2626b8ec..31a8d3ac5f598 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_api.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_api.ts @@ -9,7 +9,7 @@ import { useMemo } from 'react'; import * as Api from '../api'; import { HttpStart } from '../../../../../../src/core/public'; import { ExceptionListItemSchema, ExceptionListSchema } from '../../../common/schemas'; -import { ApiCallFindListsItemsMemoProps, ApiCallMemoProps } from '../types'; +import { ApiCallFindListsItemsMemoProps, ApiCallMemoProps, ApiListExportProps } from '../types'; import { getIdsAndNamespaces } from '../utils'; export interface ExceptionsApi { @@ -22,6 +22,7 @@ export interface ExceptionsApi { arg: ApiCallMemoProps & { onSuccess: (arg: ExceptionListSchema) => void } ) => Promise; getExceptionListsItems: (arg: ApiCallFindListsItemsMemoProps) => Promise; + exportExceptionList: (arg: ApiListExportProps) => Promise; } export const useApi = (http: HttpStart): ExceptionsApi => { @@ -67,6 +68,28 @@ export const useApi = (http: HttpStart): ExceptionsApi => { onError(error); } }, + async exportExceptionList({ + id, + listId, + namespaceType, + onError, + onSuccess, + }: ApiListExportProps): Promise { + const abortCtrl = new AbortController(); + + try { + const blob = await Api.exportExceptionList({ + http, + id, + listId, + namespaceType, + signal: abortCtrl.signal, + }); + onSuccess(blob); + } catch (error) { + onError(error); + } + }, async getExceptionItem({ id, namespaceType, diff --git a/x-pack/plugins/lists/public/exceptions/types.ts b/x-pack/plugins/lists/public/exceptions/types.ts index 02b78bc1a5e58..6a238e22344b6 100644 --- a/x-pack/plugins/lists/public/exceptions/types.ts +++ b/x-pack/plugins/lists/public/exceptions/types.ts @@ -90,6 +90,17 @@ export interface ApiCallMemoProps { onSuccess: () => void; } +// TODO: Switch to use ApiCallMemoProps +// after cleaning up exceptions/api file to +// remove unnecessary validation checks +export interface ApiListExportProps { + id: string; + listId: string; + namespaceType: NamespaceType; + onError: (err: Error) => void; + onSuccess: (blob: Blob) => void; +} + export interface ApiCallFindListsItemsMemoProps { lists: ExceptionListIdentifiers[]; filterOptions: FilterExceptionsOptions[]; @@ -156,3 +167,11 @@ export interface AddEndpointExceptionListProps { http: HttpStart; signal: AbortSignal; } + +export interface ExportExceptionListProps { + http: HttpStart; + id: string; + listId: string; + namespaceType: NamespaceType; + signal: AbortSignal; +} diff --git a/x-pack/plugins/lists/server/routes/export_exception_list_route.ts b/x-pack/plugins/lists/server/routes/export_exception_list_route.ts new file mode 100644 index 0000000000000..1394bf48cd2c7 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/export_exception_list_route.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { EXCEPTION_LIST_URL } from '../../common/constants'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { exportExceptionListQuerySchema } from '../../common/schemas'; + +import { getExceptionListClient } from './utils'; + +export const exportExceptionListRoute = (router: IRouter): void => { + router.get( + { + options: { + tags: ['access:lists-read'], + }, + path: `${EXCEPTION_LIST_URL}/_export`, + validate: { + query: buildRouteValidation(exportExceptionListQuerySchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { id, list_id: listId, namespace_type: namespaceType } = request.query; + const exceptionLists = getExceptionListClient(context); + const exceptionList = await exceptionLists.getExceptionList({ + id, + listId, + namespaceType, + }); + + if (exceptionList == null) { + return siemResponse.error({ + body: `list_id: ${listId} does not exist`, + statusCode: 400, + }); + } else { + const { exportData: exportList } = getExport([exceptionList]); + const listItems = await exceptionLists.findExceptionListItem({ + filter: undefined, + listId, + namespaceType, + page: 1, + perPage: 10000, + sortField: 'exception-list.created_at', + sortOrder: 'desc', + }); + + const { exportData: exportListItems, exportDetails } = getExport(listItems?.data ?? []); + + const responseBody = [ + exportList, + exportListItems, + { exception_list_items_details: exportDetails }, + ]; + + // TODO: Allow the API to override the name of the file to export + const fileName = exceptionList.list_id; + return response.ok({ + body: transformDataToNdjson(responseBody), + headers: { + 'Content-Disposition': `attachment; filename="${fileName}"`, + 'Content-Type': 'application/ndjson', + }, + }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; + +const transformDataToNdjson = (data: unknown[]): string => { + if (data.length !== 0) { + const dataString = data.map((dataItem) => JSON.stringify(dataItem)).join('\n'); + return `${dataString}\n`; + } else { + return ''; + } +}; + +export const getExport = ( + data: unknown[] +): { + exportData: string; + exportDetails: string; +} => { + const ndjson = transformDataToNdjson(data); + const exportDetails = JSON.stringify({ + exported_count: data.length, + }); + return { exportData: ndjson, exportDetails: `${exportDetails}\n` }; +}; diff --git a/x-pack/plugins/lists/server/routes/index.ts b/x-pack/plugins/lists/server/routes/index.ts index 0d99d726d232d..a1a54a88c0ed0 100644 --- a/x-pack/plugins/lists/server/routes/index.ts +++ b/x-pack/plugins/lists/server/routes/index.ts @@ -17,6 +17,7 @@ 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_exception_list_route'; export * from './export_list_item_route'; export * from './find_endpoint_list_item_route'; export * from './find_exception_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 163126f1277c1..1f29d0aaeeb48 100644 --- a/x-pack/plugins/lists/server/routes/init_routes.ts +++ b/x-pack/plugins/lists/server/routes/init_routes.ts @@ -22,6 +22,7 @@ import { deleteListIndexRoute, deleteListItemRoute, deleteListRoute, + exportExceptionListRoute, exportListItemRoute, findEndpointListItemRoute, findExceptionListItemRoute, @@ -76,6 +77,7 @@ export const initRoutes = (router: IRouter, config: ConfigType): void => { updateExceptionListRoute(router); deleteExceptionListRoute(router); findExceptionListRoute(router); + exportExceptionListRoute(router); // exception list items createExceptionListItemRoute(router); diff --git a/x-pack/plugins/ml/common/types/ml_url_generator.ts b/x-pack/plugins/ml/common/types/ml_url_generator.ts index fb432189c6dd3..3c70cf4c27b5d 100644 --- a/x-pack/plugins/ml/common/types/ml_url_generator.ts +++ b/x-pack/plugins/ml/common/types/ml_url_generator.ts @@ -146,30 +146,28 @@ export interface TimeSeriesExplorerGlobalState { refreshInterval?: RefreshInterval; } -export interface TimeSeriesExplorerAppState { - mlTimeSeriesExplorer?: { - forecastId?: string; - detectorIndex?: number; - entities?: Record; - zoom?: { - from?: string; - to?: string; - }; - functionDescription?: string; +export interface TimeSeriesExplorerParams { + forecastId?: string; + detectorIndex?: number; + entities?: Record; + zoom?: { + from?: string; + to?: string; }; + functionDescription?: string; +} +export interface TimeSeriesExplorerAppState { + mlTimeSeriesExplorer?: TimeSeriesExplorerParams; query?: any; } export interface TimeSeriesExplorerPageState - extends Pick, + extends TimeSeriesExplorerParams, + Pick, Pick { jobIds?: JobId[]; timeRange?: TimeRange; - detectorIndex?: number; - entities?: Record; - forecastId?: string; globalState?: MlCommonGlobalState; - functionDescription?: string; } export type TimeSeriesExplorerUrlState = MLPageState< diff --git a/x-pack/plugins/ml/common/util/group_color_utils.ts b/x-pack/plugins/ml/common/util/group_color_utils.ts index 7105919274185..b5749641d2b57 100644 --- a/x-pack/plugins/ml/common/util/group_color_utils.ts +++ b/x-pack/plugins/ml/common/util/group_color_utils.ts @@ -13,7 +13,7 @@ const COLORS = [ euiVars.euiColorVis1, euiVars.euiColorVis2, euiVars.euiColorVis3, - // euiVars.euiColorVis4, // light pink, too hard to read with white text + euiVars.euiColorVis4, euiVars.euiColorVis5, euiVars.euiColorVis6, euiVars.euiColorVis7, diff --git a/x-pack/plugins/ml/common/util/job_utils.ts b/x-pack/plugins/ml/common/util/job_utils.ts index 6063511879448..4f4d9851c4957 100644 --- a/x-pack/plugins/ml/common/util/job_utils.ts +++ b/x-pack/plugins/ml/common/util/job_utils.ts @@ -49,6 +49,22 @@ export function calculateDatafeedFrequencyDefaultSeconds(bucketSpanSeconds: numb return freq; } +export function hasRuntimeMappings(job: CombinedJob): boolean { + const hasDatafeed = + typeof job.datafeed_config === 'object' && Object.keys(job.datafeed_config).length > 0; + if (hasDatafeed) { + const runtimeMappings = + typeof job.datafeed_config.runtime_mappings === 'object' + ? Object.keys(job.datafeed_config.runtime_mappings) + : undefined; + + if (Array.isArray(runtimeMappings) && runtimeMappings.length > 0) { + return true; + } + } + return false; +} + export function isTimeSeriesViewJob(job: CombinedJob): boolean { return getSingleMetricViewerJobErrorMessage(job) === undefined; } @@ -94,10 +110,10 @@ export function isSourceDataChartableForDetector(job: CombinedJob, detectorIndex scriptFields.indexOf(dtr.over_field_name!) === -1; } - // We cannot plot the source data for some specific aggregation configurations const hasDatafeed = typeof job.datafeed_config === 'object' && Object.keys(job.datafeed_config).length > 0; if (hasDatafeed) { + // We cannot plot the source data for some specific aggregation configurations const aggs = getDatafeedAggregations(job.datafeed_config); if (aggs !== undefined) { const aggBucketsName = getAggregationBucketsName(aggs); @@ -110,6 +126,11 @@ export function isSourceDataChartableForDetector(job: CombinedJob, detectorIndex } } } + + // We also cannot plot the source data if they datafeed uses any field defined by runtime_mappings + if (hasRuntimeMappings(job)) { + return false; + } } } @@ -149,6 +170,12 @@ export function isModelPlotChartableForDetector(job: Job, detectorIndex: number) // Returns a reason to indicate why the job configuration is not supported // if the result is undefined, that means the single metric job should be viewable export function getSingleMetricViewerJobErrorMessage(job: CombinedJob): string | undefined { + // if job has runtime mappings with no model plot + if (hasRuntimeMappings(job) && !job.model_plot_config?.enabled) { + return i18n.translate('xpack.ml.timeSeriesJob.jobWithRunTimeMessage', { + defaultMessage: 'the datafeed contains runtime fields and model plot is disabled', + }); + } // only allow jobs with at least one detector whose function corresponds to // an ES aggregation which can be viewed in the single metric view and which // doesn't use a scripted field which can be very difficult or impossible to diff --git a/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.tsx b/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.tsx index 059ab48daa27e..0fc1d458399dd 100644 --- a/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.tsx +++ b/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.tsx @@ -51,7 +51,7 @@ function optionValueToInterval(value: string) { return interval; } -const TABLE_INTERVAL_DEFAULT = optionValueToInterval('auto'); +export const TABLE_INTERVAL_DEFAULT = optionValueToInterval('auto'); export const useTableInterval = (): [TableInterval, (v: TableInterval) => void] => { return usePageUrlState('mlSelectInterval', TABLE_INTERVAL_DEFAULT); diff --git a/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.test.tsx b/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.test.tsx index e2aa13f6019ed..b882531eb75b0 100644 --- a/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.test.tsx +++ b/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.test.tsx @@ -5,15 +5,31 @@ */ import { mount } from 'enzyme'; +import { render } from '@testing-library/react'; import React from 'react'; -import { MemoryRouter } from 'react-router-dom'; import { EuiSuperDatePicker } from '@elastic/eui'; +import { useUrlState } from '../../../util/url_state'; import { mlTimefilterRefresh$ } from '../../../services/timefilter_refresh_service'; import { DatePickerWrapper } from './date_picker_wrapper'; +jest.mock('@elastic/eui', () => { + const EuiSuperDatePickerMock = jest.fn(() => { + return null; + }); + return { EuiSuperDatePicker: EuiSuperDatePickerMock }; +}); + +jest.mock('../../../util/url_state', () => { + return { + useUrlState: jest.fn(() => { + return [{ refreshInterval: { value: 0, pause: true } }, jest.fn()]; + }), + }; +}); + jest.mock('../../../contexts/kibana', () => ({ useMlKibana: () => { return { @@ -25,9 +41,11 @@ jest.mock('../../../contexts/kibana', () => ({ timefilter: { getRefreshInterval: jest.fn(), setRefreshInterval: jest.fn(), - getTime: jest.fn(), - isAutoRefreshSelectorEnabled: jest.fn(), - isTimeRangeSelectorEnabled: jest.fn(), + getTime: jest.fn(() => { + return { from: '', to: '' }; + }), + isAutoRefreshSelectorEnabled: jest.fn(() => true), + isTimeRangeSelectorEnabled: jest.fn(() => true), getRefreshIntervalUpdate$: jest.fn(), getTimeUpdate$: jest.fn(), getEnabledUpdated$: jest.fn(), @@ -41,11 +59,12 @@ jest.mock('../../../contexts/kibana', () => ({ }, })); -const noop = () => {}; +const MockedEuiSuperDatePicker = EuiSuperDatePicker as jest.MockedClass; describe('Navigation Menu: ', () => { beforeEach(() => { jest.useFakeTimers(); + MockedEuiSuperDatePicker.mockClear(); }); afterEach(() => { @@ -56,66 +75,22 @@ describe('Navigation Menu: ', () => { const refreshListener = jest.fn(); const refreshSubscription = mlTimefilterRefresh$.subscribe(refreshListener); - const wrapper = mount( - - - - ); + const wrapper = mount(); expect(wrapper.find(DatePickerWrapper)).toHaveLength(1); expect(refreshListener).toBeCalledTimes(0); refreshSubscription.unsubscribe(); }); - // The following tests are written against EuiSuperDatePicker - // instead of DatePickerWrapper. DatePickerWrapper uses hooks and we cannot write tests - // with async hook updates yet until React 16.9 is available. - test('Listen for consecutive super date picker refreshs.', async () => { - const onRefresh = jest.fn(); - - const componentRefresh = mount( - - ); - - const instanceRefresh = componentRefresh.instance(); - - jest.advanceTimersByTime(10); - // @ts-ignore - await instanceRefresh.asyncInterval.__pendingFn; - jest.advanceTimersByTime(10); - // @ts-ignore - await instanceRefresh.asyncInterval.__pendingFn; - - expect(onRefresh).toBeCalledTimes(2); - }); + test('should not allow disabled pause with 0 refresh interval', () => { + // arrange + (useUrlState as jest.Mock).mockReturnValue([{ refreshInterval: { pause: false, value: 0 } }]); + + // act + render(); - test('Switching refresh interval to pause should stop onRefresh being called.', async () => { - const onRefresh = jest.fn(); - - const componentRefresh = mount( - - ); - - const instanceRefresh = componentRefresh.instance(); - - jest.advanceTimersByTime(10); - // @ts-ignore - await instanceRefresh.asyncInterval.__pendingFn; - componentRefresh.setProps({ isPaused: true, refreshInterval: 0 }); - jest.advanceTimersByTime(10); - // @ts-ignore - await instanceRefresh.asyncInterval.__pendingFn; - - expect(onRefresh).toBeCalledTimes(1); + // assert + const calledWith = MockedEuiSuperDatePicker.mock.calls[0][0]; + expect(calledWith.isPaused).toBe(true); }); }); diff --git a/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx b/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx index a4dc78ea53a77..dc046241f82b9 100644 --- a/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useCallback, useEffect, useState } from 'react'; +import React, { FC, useCallback, useEffect, useState } from 'react'; import { Subscription } from 'rxjs'; import { debounce } from 'lodash'; @@ -122,24 +122,25 @@ export const DatePickerWrapper: FC = () => { setRefreshInterval({ pause, value }); } - return ( - - {(isAutoRefreshSelectorEnabled || isTimeRangeSelectorEnabled) && ( -
- -
- )} -
- ); + /** + * Enforce pause when it's set to false with 0 refresh interval. + */ + const isPaused = refreshInterval.pause || (!refreshInterval.pause && !refreshInterval.value); + + return isAutoRefreshSelectorEnabled || isTimeRangeSelectorEnabled ? ( +
+ +
+ ) : null; }; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap b/x-pack/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap index 64638fcd23734..ecb7fc07711e8 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap @@ -596,8 +596,8 @@ Object { "loading": false, "metricFieldName": "responsetime", "metricFunction": "avg", - "plotEarliest": 1486560600000, - "plotLatest": 1486765800000, + "plotEarliest": 1486542600000, + "plotLatest": 1486783800000, "selectedEarliest": 1486656000000, "selectedLatest": 1486670399999, "timeField": "@timestamp", diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js index ee5247d193e8a..774372f678c9b 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiButtonEmpty, @@ -54,14 +54,33 @@ function getChartId(series) { } // Wrapper for a single explorer chart -function ExplorerChartContainer({ series, severity, tooManyBuckets, wrapLabel, mlUrlGenerator }) { - const redirectToSingleMetricViewer = useCallback(async () => { - const singleMetricViewerLink = await getExploreSeriesLink(mlUrlGenerator, series); - addItemToRecentlyAccessed('timeseriesexplorer', series.jobId, singleMetricViewerLink); - - window.open(singleMetricViewerLink, '_blank'); - }, [mlUrlGenerator]); - +function ExplorerChartContainer({ + series, + severity, + tooManyBuckets, + wrapLabel, + mlUrlGenerator, + basePath, +}) { + const [explorerSeriesLink, setExplorerSeriesLink] = useState(); + + useEffect(() => { + let isCancelled = false; + const generateLink = async () => { + const singleMetricViewerLink = await getExploreSeriesLink(mlUrlGenerator, series); + if (!isCancelled) { + setExplorerSeriesLink(singleMetricViewerLink); + } + }; + generateLink(); + return () => { + isCancelled = true; + }; + }, [mlUrlGenerator, series]); + + const addToRecentlyAccessed = useCallback(() => { + addItemToRecentlyAccessed('timeseriesexplorer', series.jobId, explorerSeriesLink); + }, [explorerSeriesLink]); const { detectorLabel, entityFields } = series; const chartType = getChartType(series); @@ -111,16 +130,22 @@ function ExplorerChartContainer({ series, severity, tooManyBuckets, wrapLabel, m /> )} - - - - - + {explorerSeriesLink && ( + + {/* href needs to be full link with base path while ChromeRecentlyAccessed requires only relative path */} + {/* disabling because we need button to behave as link and to have a callback */} + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + + + + + )}
@@ -170,14 +195,13 @@ export const ExplorerChartsContainerUI = ({ }) => { const { services: { - application: { navigateToApp }, - + http: { basePath }, share: { urlGenerators: { getUrlGenerator }, }, }, } = kibana; - const mlUrlGenerator = getUrlGenerator(ML_APP_URL_GENERATOR); + const mlUrlGenerator = useMemo(() => getUrlGenerator(ML_APP_URL_GENERATOR), [getUrlGenerator]); // doesn't allow a setting of `columns={1}` when chartsPerRow would be 1. // If that's the case we trick it doing that with the following settings: @@ -201,8 +225,8 @@ export const ExplorerChartsContainerUI = ({ severity={severity} tooManyBuckets={tooManyBuckets} wrapLabel={wrapLabel} - navigateToApp={navigateToApp} mlUrlGenerator={mlUrlGenerator} + basePath={basePath.get()} /> ))} diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js index 2da212c8f2f29..96004516135d0 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js @@ -56,6 +56,11 @@ describe('ExplorerChartsContainer', () => { const kibanaContextMock = { services: { application: { navigateToApp: jest.fn() }, + http: { + basePath: { + get: jest.fn(), + }, + }, share: { urlGenerators: { getUrlGenerator: jest.fn() }, }, diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js index 47087e776d6dd..a2c530c9ca494 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js @@ -29,6 +29,7 @@ import { explorerService } from '../explorer_dashboard_service'; import { CHART_TYPE } from '../explorer_constants'; import { i18n } from '@kbn/i18n'; +import { SWIM_LANE_LABEL_WIDTH } from '../swimlane_container'; export function getDefaultChartsData() { return { @@ -57,15 +58,14 @@ export const anomalyDataChange = function ( ) { const data = getDefaultChartsData(); + const containerWith = chartsContainerWidth + SWIM_LANE_LABEL_WIDTH; + const filteredRecords = anomalyRecords.filter((record) => { return Number(record.record_score) >= severity; }); const [allSeriesRecords, errorMessages] = processRecordsForDisplay(filteredRecords); // Calculate the number of charts per row, depending on the width available, to a max of 4. - let chartsPerRow = Math.min( - Math.max(Math.floor(chartsContainerWidth / 550), 1), - MAX_CHARTS_PER_ROW - ); + let chartsPerRow = Math.min(Math.max(Math.floor(containerWith / 550), 1), MAX_CHARTS_PER_ROW); if (allSeriesRecords.length === 1) { chartsPerRow = 1; } @@ -81,7 +81,7 @@ export const anomalyDataChange = function ( // Calculate the time range of the charts, which is a function of the chart width and max job bucket span. data.tooManyBuckets = false; - const chartWidth = Math.floor(chartsContainerWidth / chartsPerRow); + const chartWidth = Math.floor(containerWith / chartsPerRow); const { chartRange, tooManyBuckets } = calculateChartRange( seriesConfigs, selectedEarliestMs, diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index 101d4857a89b1..145f6cc0fcf7a 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -46,11 +46,11 @@ import { useUiSettings } from '../contexts/kibana'; /** * Ignore insignificant resize, e.g. browser scrollbar appearance. */ -const RESIZE_IGNORED_DIFF_PX = 20; const RESIZE_THROTTLE_TIME_MS = 500; const CELL_HEIGHT = 30; const LEGEND_HEIGHT = 34; const Y_AXIS_HEIGHT = 24; +export const SWIM_LANE_LABEL_WIDTH = 200; export function isViewBySwimLaneData(arg: any): arg is ViewBySwimLaneData { return arg && arg.hasOwnProperty('cardinality'); @@ -167,12 +167,9 @@ export const SwimlaneContainer: FC = ({ const resizeHandler = useCallback( throttle((e: { width: number; height: number }) => { - const labelWidth = 200; - const resultNewWidth = e.width - labelWidth; - if (Math.abs(resultNewWidth - chartWidth) > RESIZE_IGNORED_DIFF_PX) { - setChartWidth(resultNewWidth); - onResize(resultNewWidth); - } + const resultNewWidth = e.width - SWIM_LANE_LABEL_WIDTH; + setChartWidth(resultNewWidth); + onResize(resultNewWidth); }, RESIZE_THROTTLE_TIME_MS), [chartWidth] ); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js index ec5ef6fce26b5..97b705177ed85 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js @@ -19,6 +19,7 @@ import { import { ml } from '../../../../../services/ml_api_service'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { tabColor } from '../../../../../../../common/util/group_color_utils'; export class JobDetails extends Component { constructor(props) { @@ -43,7 +44,7 @@ export class JobDetails extends Component { ml.jobs .groups() .then((resp) => { - const groups = resp.map((g) => ({ label: g.id })); + const groups = resp.map((g) => ({ label: g.id, color: tabColor(g.id) })); this.setState({ groups }); }) .catch((error) => { @@ -53,7 +54,9 @@ export class JobDetails extends Component { static getDerivedStateFromProps(props) { const selectedGroups = - props.jobGroups !== undefined ? props.jobGroups.map((g) => ({ label: g })) : []; + props.jobGroups !== undefined + ? props.jobGroups.map((g) => ({ label: g, color: tabColor(g) })) + : []; return { description: props.jobDescription, diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx index 1b1bea889925f..71be812662299 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx @@ -17,7 +17,6 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -// @ts-ignore import { JobGroup } from '../job_group'; import { useMlKibana } from '../../../../contexts/kibana'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_group/index.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_group/index.ts similarity index 100% rename from x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_group/index.js rename to x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_group/index.ts diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_group/job_group.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_group/job_group.js deleted file mode 100644 index e8892c076c7a9..0000000000000 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_group/job_group.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { tabColor } from '../../../../../../common/util/group_color_utils'; - -import PropTypes from 'prop-types'; -import React from 'react'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; - -export function JobGroup({ name }) { - return ( -
- {name} -
- ); -} -JobGroup.propTypes = { - name: PropTypes.string.isRequired, -}; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_group/job_group.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_group/job_group.tsx new file mode 100644 index 0000000000000..eab3e5ae3c1c7 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_group/job_group.tsx @@ -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 React, { FC } from 'react'; +import { EuiBadge } from '@elastic/eui'; +import { tabColor } from '../../../../../../common/util/group_color_utils'; + +export const JobGroup: FC<{ name: string }> = ({ name }) => ( + + {name} + +); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_id_link.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_id_link.tsx index f1c82dbb83eb4..b81934630facd 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_id_link.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_id_link.tsx @@ -55,7 +55,8 @@ export const AnomalyDetectionJobIdLink = (props: AnomalyDetectionJobIdLinkProps) if (isGroupIdLink(props)) { return ( - + // Set margin-left to match EuiBadge (in JobGroup) built-in left margin for consistent badge spacing in management and plugin jobs list + ); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/group_list.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/group_list.js index d989064c5057f..b17172aba6a95 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/group_list.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/group_list.js @@ -7,7 +7,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { EuiIcon, keys } from '@elastic/eui'; +import { EuiFlexItem, EuiFlexGroup, EuiIcon, keys } from '@elastic/eui'; import { JobGroup } from '../../../job_group'; @@ -97,8 +97,14 @@ export class GroupList extends Component { onClick={() => this.selectGroup(g)} ref={(ref) => this.setRef(ref, index)} > - - + + + + + + + +
))}
diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx index b60a265560455..97ea27c5fe40a 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx @@ -5,12 +5,44 @@ */ import React from 'react'; -import { MemoryRouter } from 'react-router-dom'; import { render } from '@testing-library/react'; - import { I18nProvider } from '@kbn/i18n/react'; - import { TimeSeriesExplorerUrlStateManager } from './timeseriesexplorer'; +import { TimeSeriesExplorer } from '../../timeseriesexplorer'; +import { TimeSeriesExplorerPage } from '../../timeseriesexplorer/timeseriesexplorer_page'; +import { TimeseriesexplorerNoJobsFound } from '../../timeseriesexplorer/components/timeseriesexplorer_no_jobs_found'; + +jest.mock('../../services/toast_notification_service'); + +jest.mock('../../timeseriesexplorer', () => ({ + TimeSeriesExplorer: jest.fn(() => { + return null; + }), +})); + +jest.mock('../../timeseriesexplorer/timeseriesexplorer_page', () => ({ + TimeSeriesExplorerPage: jest.fn(({ children }) => { + return <>{children}; + }), +})); + +jest.mock('../../timeseriesexplorer/components/timeseriesexplorer_no_jobs_found', () => ({ + TimeseriesexplorerNoJobsFound: jest.fn(() => { + return null; + }), +})); + +const MockedTimeSeriesExplorer = TimeSeriesExplorer as jest.MockedClass; +const MockedTimeSeriesExplorerPage = TimeSeriesExplorerPage as jest.MockedFunction< + typeof TimeSeriesExplorerPage +>; +const MockedTimeseriesexplorerNoJobsFound = TimeseriesexplorerNoJobsFound as jest.MockedFunction< + typeof TimeseriesexplorerNoJobsFound +>; + +jest.mock('../../util/url_state'); + +jest.mock('../../timeseriesexplorer/hooks/use_timeseriesexplorer_url_state'); jest.mock('../../contexts/kibana/kibana_context', () => { // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -59,27 +91,22 @@ jest.mock('../../contexts/kibana/kibana_context', () => { }; }); -jest.mock('../../util/dependency_cache', () => ({ - getToastNotifications: () => ({ addSuccess: jest.fn(), addDanger: jest.fn() }), -})); - -jest.mock('../../../../shared_imports'); - describe('TimeSeriesExplorerUrlStateManager', () => { - test('Initial render shows "No single metric jobs found"', () => { + test('should render TimeseriesexplorerNoJobsFound when no jobs provided', () => { const props = { config: { get: () => 'Browser' }, jobsWithTimeRange: [], }; - const { container } = render( + render( - - - + ); - expect(container.textContent).toContain('No single metric jobs found'); + // assert + expect(MockedTimeSeriesExplorer).not.toHaveBeenCalled(); + expect(MockedTimeSeriesExplorerPage).toHaveBeenCalled(); + expect(MockedTimeseriesexplorerNoJobsFound).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx index 7de59cba495af..857e894d404ae 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -11,7 +11,7 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; -import { NavigateToPath } from '../../contexts/kibana'; +import { NavigateToPath, useNotifications } from '../../contexts/kibana'; import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; @@ -93,6 +93,7 @@ export const TimeSeriesExplorerUrlStateManager: FC { + const { toasts } = useNotifications(); const toastNotificationService = useToastNotificationService(); const [ timeSeriesExplorerUrlState, @@ -249,7 +250,12 @@ export const TimeSeriesExplorerUrlStateManager: FC { + return [{}, jest.fn()]; +}); diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts index 8159dbb8ade06..26525505420de 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FC } from 'react'; +import React from 'react'; import { TimeRangeBounds } from '../explorer/explorer_utils'; -declare const TimeSeriesExplorer: FC<{ +interface Props { appStateHandler: (action: string, payload: any) => void; autoZoomDuration: number; bounds: TimeRangeBounds; @@ -21,4 +21,7 @@ declare const TimeSeriesExplorer: FC<{ tableInterval: string; tableSeverity: number; zoom?: { from?: string; to?: string }; -}>; +} + +// eslint-disable-next-line react/prefer-stateless-function +declare class TimeSeriesExplorer extends React.Component {} diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.ts index cd8a10a9e1f99..1781d0ee6369b 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.ts @@ -8,8 +8,7 @@ import { difference, without } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { getToastNotifications } from '../../util/dependency_cache'; - +import { ToastsStart } from 'kibana/public'; import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; import { getTimeRangeFromSelection } from '../../components/job_selector/job_select_service_utils'; @@ -24,9 +23,9 @@ import { createTimeSeriesJobData } from './timeseriesexplorer_utils'; export function validateJobSelection( jobsWithTimeRange: MlJobWithTimeRange[], selectedJobIds: string[], - setGlobalState: (...args: any) => void + setGlobalState: (...args: any) => void, + toastNotifications: ToastsStart ) { - const toastNotifications = getToastNotifications(); const jobs = createTimeSeriesJobData(mlJobService.jobs); const timeSeriesJobIds: string[] = jobs.map((j: any) => j.id); diff --git a/x-pack/plugins/ml/public/application/util/__mocks__/url_state.tsx b/x-pack/plugins/ml/public/application/util/__mocks__/url_state.tsx new file mode 100644 index 0000000000000..cb237b951d8dd --- /dev/null +++ b/x-pack/plugins/ml/public/application/util/__mocks__/url_state.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 { AppStateKey } from '../url_state'; +import { TABLE_INTERVAL_DEFAULT } from '../../components/controls/select_interval/select_interval'; + +export const useUrlState = jest.fn((accessor: '_a' | '_g') => { + if (accessor === '_g') { + return [{ refreshInterval: { value: 0, pause: true } }, jest.fn()]; + } +}); + +export const usePageUrlState = jest.fn((pageKey: AppStateKey) => { + let state: unknown; + switch (pageKey) { + case 'timeseriesexplorer': + state = {}; + break; + case 'mlSelectInterval': + state = TABLE_INTERVAL_DEFAULT; + break; + } + return [state, jest.fn()]; +}); diff --git a/x-pack/plugins/ml/public/application/util/chart_utils.js b/x-pack/plugins/ml/public/application/util/chart_utils.js index d142d2e246659..402c922a0034f 100644 --- a/x-pack/plugins/ml/public/application/util/chart_utils.js +++ b/x-pack/plugins/ml/public/application/util/chart_utils.js @@ -264,7 +264,7 @@ export async function getExploreSeriesLink(mlUrlGenerator, series) { }, }, }, - excludeBasePath: false, + excludeBasePath: true, }); return url; } diff --git a/x-pack/plugins/ml/public/application/util/url_state.tsx b/x-pack/plugins/ml/public/application/util/url_state.tsx index 569e7bcc7b7e1..b565a0f7b7a73 100644 --- a/x-pack/plugins/ml/public/application/util/url_state.tsx +++ b/x-pack/plugins/ml/public/application/util/url_state.tsx @@ -73,7 +73,9 @@ export const urlStateStore = createContext({ searchString: '', setUrlState: () => {}, }); + const { Provider } = urlStateStore; + export const UrlStateProvider: FC = ({ children }) => { const history = useHistory(); const { search: searchString } = useLocation(); @@ -164,7 +166,7 @@ export const useUrlState = (accessor: Accessor) => { type LegacyUrlKeys = 'mlExplorerSwimlane'; -type AppStateKey = +export type AppStateKey = | 'mlSelectSeverity' | 'mlSelectInterval' | 'mlAnomaliesTable' diff --git a/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts index d2814bd63b0b0..b6a3ca0ce7139 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts @@ -163,6 +163,7 @@ export function createSingleMetricViewerUrl( entities, globalState, functionDescription, + zoom, } = params; let queryState: Partial = {}; @@ -193,6 +194,10 @@ export function createSingleMetricViewerUrl( mlTimeSeriesExplorer.functionDescription = functionDescription; } + if (zoom !== undefined) { + mlTimeSeriesExplorer.zoom = zoom; + } + appState.mlTimeSeriesExplorer = mlTimeSeriesExplorer; if (query) diff --git a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts index 7dcd901c2c0ef..21da0424cdca0 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts @@ -161,7 +161,7 @@ describe('MlUrlGenerator', () => { }, }); expect(url).toBe( - "/app/ml/timeseriesexplorer?_g=(ml:(jobIds:!(logs_categorization_1)),refreshInterval:(pause:!f,value:0),time:(from:'2020-07-12T00:39:02.912Z',mode:absolute,to:'2020-07-22T15:52:18.613Z'))&_a=(timeseriesexplorer:(mlTimeSeriesExplorer:(detectorIndex:0,entities:(mlcategory:'2')),query:(query_string:(analyze_wildcard:!t,query:'*'))))" + "/app/ml/timeseriesexplorer?_g=(ml:(jobIds:!(logs_categorization_1)),refreshInterval:(pause:!f,value:0),time:(from:'2020-07-12T00:39:02.912Z',mode:absolute,to:'2020-07-22T15:52:18.613Z'))&_a=(timeseriesexplorer:(mlTimeSeriesExplorer:(detectorIndex:0,entities:(mlcategory:'2'),zoom:(from:'2020-07-20T23:58:29.367Z',to:'2020-07-21T11:00:13.173Z')),query:(query_string:(analyze_wildcard:!t,query:'*'))))" ); }); }); diff --git a/x-pack/plugins/ml/server/models/job_service/groups.ts b/x-pack/plugins/ml/server/models/job_service/groups.ts index 59090f30ccca9..f6073ae7071b0 100644 --- a/x-pack/plugins/ml/server/models/job_service/groups.ts +++ b/x-pack/plugins/ml/server/models/job_service/groups.ts @@ -72,7 +72,9 @@ export function groupsProvider(mlClient: MlClient) { }); } - return Object.keys(groups).map((g) => groups[g]); + return Object.keys(groups) + .sort() + .map((g) => groups[g]); } async function updateGroups(jobs: Job[]) { diff --git a/x-pack/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap index ffb1620b60fa3..8f820252d5b6b 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap @@ -2,7 +2,7 @@ exports[`NoData should show a default message if reason is unknown 1`] = `

{ expect(getByText('Largest contentful paint')).toBeInTheDocument(); expect(getByText('1.94 s')).toBeInTheDocument(); expect(getByText('14 ms')).toBeInTheDocument(); - expect(getByText('0.01')).toBeInTheDocument(); + expect(getByText('0.010')).toBeInTheDocument(); // LCP Rank Values expect(getByText('Good (65%)')).toBeInTheDocument(); diff --git a/x-pack/plugins/observability/public/components/app/section/ux/mock_data/ux.mock.ts b/x-pack/plugins/observability/public/components/app/section/ux/mock_data/ux.mock.ts index 017f385d36735..bbe81699e999d 100644 --- a/x-pack/plugins/observability/public/components/app/section/ux/mock_data/ux.mock.ts +++ b/x-pack/plugins/observability/public/components/app/section/ux/mock_data/ux.mock.ts @@ -9,7 +9,7 @@ import { UxFetchDataResponse } from '../../../../../typings'; export const response: UxFetchDataResponse = { appLink: '/app/ux', coreWebVitals: { - cls: '0.01', + cls: 0.01, fid: 13.5, lcp: 1942.6666666666667, tbt: 281.55833333333334, diff --git a/x-pack/plugins/observability/public/components/shared/core_web_vitals/__stories__/core_vitals.stories.tsx b/x-pack/plugins/observability/public/components/shared/core_web_vitals/__stories__/core_vitals.stories.tsx index 26cf9c144b4a1..208c840b403e9 100644 --- a/x-pack/plugins/observability/public/components/shared/core_web_vitals/__stories__/core_vitals.stories.tsx +++ b/x-pack/plugins/observability/public/components/shared/core_web_vitals/__stories__/core_vitals.stories.tsx @@ -33,13 +33,26 @@ export default { ], }; -export function Basic() { +export function NoDataAvailable() { + return ( + + ); +} + +export function OneHundredPercentGood() { return ( ); diff --git a/x-pack/plugins/observability/public/components/shared/core_web_vitals/core_vital_item.test.tsx b/x-pack/plugins/observability/public/components/shared/core_web_vitals/core_vital_item.test.tsx new file mode 100644 index 0000000000000..346355e11c6ef --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/core_web_vitals/core_vital_item.test.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { render } from '../../../utils/test_helper'; +import { CoreVitalItem } from './core_vital_item'; +import { NO_DATA } from './translations'; + +describe('CoreVitalItem', () => { + const value = '0.005'; + const title = 'Cumulative Layout Shift'; + const thresholds = { bad: '0.25', good: '0.1' }; + const loading = false; + const helpLabel = 'sample help label'; + + it('renders if value is truthy', () => { + const { getByText } = render( + + ); + + expect(getByText(title)).toBeInTheDocument(); + expect(getByText(value)).toBeInTheDocument(); + expect(getByText('Good (85%)')).toBeInTheDocument(); + expect(getByText('Needs improvement (10%)')).toBeInTheDocument(); + expect(getByText('Poor (5%)')).toBeInTheDocument(); + }); + + it('renders loading state when loading is truthy', () => { + const { queryByText, getByText } = render( + + ); + + expect(queryByText(value)).not.toBeInTheDocument(); + expect(getByText('--')).toBeInTheDocument(); + }); + + it('renders no data UI if value is falsey and loading is falsey', () => { + const { getByText } = render( + + ); + + expect(getByText(NO_DATA)).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/core_web_vitals/core_vital_item.tsx b/x-pack/plugins/observability/public/components/shared/core_web_vitals/core_vital_item.tsx index 18831565b8784..23dd0b86a235b 100644 --- a/x-pack/plugins/observability/public/components/shared/core_web_vitals/core_vital_item.tsx +++ b/x-pack/plugins/observability/public/components/shared/core_web_vitals/core_vital_item.tsx @@ -88,12 +88,14 @@ export function CoreVitalItem({ const biggestValIndex = ranks.indexOf(Math.max(...ranks)); - if ((value === null || value !== undefined) && ranks[0] === 100 && !loading) { + if (!value && !loading) { return ; } + return ( <> { title: 'User Experience', appLink: '/ux', coreWebVitals: { - cls: '0.01', + cls: 0.01, fid: 5, lcp: 1464.3333333333333, tbt: 232.92166666666665, @@ -298,7 +298,7 @@ describe('registerDataHandler', () => { title: 'User Experience', appLink: '/ux', coreWebVitals: { - cls: '0.01', + cls: 0.01, fid: 5, lcp: 1464.3333333333333, tbt: 232.92166666666665, diff --git a/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts b/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts index 1211d4c2cf1c3..6d7f5c2e367fa 100644 --- a/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts +++ b/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient, SearchResponse } from 'kibana/server'; import { ReportingConfig } from '../'; import { ExportTypesRegistry } from '../lib/export_types_registry'; import { GetLicense } from './'; @@ -18,7 +18,7 @@ import { KeyCountBucket, RangeStats, ReportingUsageType, - SearchResponse, + ReportingUsageSearchResponse, StatusByAppBucket, } from './types'; @@ -99,7 +99,9 @@ type RangeStatSets = Partial & { last7Days: Partial; }; -async function handleResponse(response: SearchResponse): Promise> { +type ESResponse = Partial>; + +async function handleResponse(response: ESResponse): Promise> { const buckets = get(response, 'aggregations.ranges.buckets'); if (!buckets) { return {}; @@ -118,7 +120,7 @@ async function handleResponse(response: SearchResponse): Promise { const reportingIndex = config.get('index'); @@ -165,8 +167,9 @@ export async function getReportingUsage( }; const featureAvailability = await getLicense(); - return callCluster('search', params) - .then((response: SearchResponse) => handleResponse(response)) + return esClient + .search(params) + .then(({ body: response }) => handleResponse(response)) .then( (usage: Partial): ReportingUsageType => { // Allow this to explicitly throw an exception if/when this config is deprecated, diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts index 7cae5e9b6f956..8b0c442c12b97 100644 --- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts @@ -59,7 +59,7 @@ const getResponseMock = (base = {}) => base; const getMockFetchClients = (resp: any) => { const fetchParamsMock = createCollectorFetchContextMock(); - fetchParamsMock.callCluster.mockResolvedValue(resp); + fetchParamsMock.esClient.search = jest.fn().mockResolvedValue({ body: resp }); return fetchParamsMock; }; describe('license checks', () => { diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts index f4209730b68ce..547c331784c5f 100644 --- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts @@ -26,9 +26,9 @@ export function getReportingUsageCollector( ) { return usageCollection.makeUsageCollector({ type: 'reporting', - fetch: ({ callCluster }: CollectorFetchContext) => { + fetch: ({ esClient }: CollectorFetchContext) => { const config = reporting.getConfig(); - return getReportingUsage(config, getLicense, callCluster, exportTypesRegistry); + return getReportingUsage(config, getLicense, esClient, exportTypesRegistry); }, isReady, schema: reportingSchema, diff --git a/x-pack/plugins/reporting/server/usage/types.ts b/x-pack/plugins/reporting/server/usage/types.ts index 1ff680eff8eaf..fe7838240f2fa 100644 --- a/x-pack/plugins/reporting/server/usage/types.ts +++ b/x-pack/plugins/reporting/server/usage/types.ts @@ -152,7 +152,7 @@ export interface AggregationResultBuckets { doc_count: number; } -export interface SearchResponse { +export interface ReportingUsageSearchResponse { aggregations: { ranges: { buckets: { diff --git a/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap b/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap index 194f6301b11ea..75886b4573edd 100644 --- a/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap +++ b/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ResetSessionPage renders as expected 1`] = `"MockedFonts

You do not have permission to access the requested page

Either go back to the previous page or log in as a different user.

"`; +exports[`ResetSessionPage renders as expected 1`] = `"MockedFonts

You do not have permission to access the requested page

Either go back to the previous page or log in as a different user.

"`; 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 aa114ff074898..26d13b13f40cb 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -408,12 +408,23 @@ export type ImportTimelineResultSchema = runtimeTypes.TypeOf; -export type TimelineExpandedEvent = TimelineExpandedEventType | EmptyObject; +export type TimelineExpandedEventType = + | { + eventId: string; + indexName: string; + } + | EmptyObject; + +export type TimelineExpandedEvent = { + [tab in TimelineTabs]?: TimelineExpandedEventType; +}; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.test.tsx index 0c9a725f918e5..a92fc793c796e 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.test.tsx @@ -8,11 +8,11 @@ import React from 'react'; import { mount } from 'enzyme'; import { CaseStatuses } from '../../../../../case/common/api'; -import { CasesTableFilters } from './table_filters'; import { TestProviders } from '../../../common/mock'; import { useGetTags } from '../../containers/use_get_tags'; import { useGetReporters } from '../../containers/use_get_reporters'; import { DEFAULT_FILTER_OPTIONS } from '../../containers/use_get_cases'; +import { CasesTableFilters } from './table_filters'; jest.mock('../../containers/use_get_reporters'); jest.mock('../../containers/use_get_tags'); @@ -151,4 +151,20 @@ describe('CasesTableFilters ', () => { ); expect(onFilterChanged).toHaveBeenCalledWith({ reporters: [{ username: 'casetester' }] }); }); + + it('StatusFilterWrapper should have a fixed width of 180px', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="status-filter-wrapper"]').first()).toHaveStyleRule( + 'flex-basis', + '180px', + { + modifier: '&&', + } + ); + }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx index f5ec0bf144154..768ad300c02e6 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx @@ -6,6 +6,7 @@ import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { isEqual } from 'lodash/fp'; +import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiFilterGroup } from '@elastic/eui'; import { CaseStatuses } from '../../../../../case/common/api'; @@ -25,6 +26,13 @@ interface CasesTableFiltersProps { setFilterRefetch: (val: () => void) => void; } +// Fix the width of the status dropdown to prevent hiding long text items +const StatusFilterWrapper = styled(EuiFlexItem)` + && { + flex-basis: 180px; + } +`; + /** * Collection of filters for filtering data within the CasesTable. Contains search bar, * and tag selection @@ -131,23 +139,27 @@ const CasesTableFiltersComponent = ({ ); return ( - - - - - - + + + + + + + + + + diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/auto_download.test.tsx b/x-pack/plugins/security_solution/public/common/components/auto_download/auto_download.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/auto_download.test.tsx rename to x-pack/plugins/security_solution/public/common/components/auto_download/auto_download.test.tsx diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/auto_download.tsx b/x-pack/plugins/security_solution/public/common/components/auto_download/auto_download.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/auto_download.tsx rename to x-pack/plugins/security_solution/public/common/components/auto_download/auto_download.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap index 973d067d9e379..e9b11d9bcdf71 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap @@ -577,6 +577,7 @@ exports[`EventDetails rendering should match snapshot 1`] = ` } eventId="Y-6TfmcB0WOhS6qyMv3s" timelineId="test" + timelineTabType="query" /> , "id": "table-view", @@ -1157,6 +1158,7 @@ exports[`EventDetails rendering should match snapshot 1`] = ` } eventId="Y-6TfmcB0WOhS6qyMv3s" timelineId="test" + timelineTabType="query" /> , "id": "table-view", diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx index 4659006050781..9ab286b120dd3 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { waitFor } from '@testing-library/dom'; import { ReactWrapper, shallow } from 'enzyme'; import React from 'react'; @@ -16,7 +17,7 @@ import { mockBrowserFields } from '../../containers/source/mock'; import { useMountAppended } from '../../utils/use_mount_appended'; import { mockAlertDetailsData } from './__mocks__'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; -import { waitFor } from '@testing-library/dom'; +import { TimelineTabs } from '../../../../common/types/timeline'; jest.mock('../link_to'); describe('EventDetails', () => { @@ -27,6 +28,7 @@ describe('EventDetails', () => { id: mockDetailItemDataId, isAlert: false, onViewSelected: jest.fn(), + timelineTabType: TimelineTabs.query, timelineId: 'test', view: EventsViewType.summaryView, }; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index 291893fe682b4..123a3fa7b610b 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -14,6 +14,7 @@ import { EventFieldsBrowser } from './event_fields_browser'; import { JsonView } from './json_view'; import * as i18n from './translations'; import { SummaryView } from './summary_view'; +import { TimelineTabs } from '../../../../common/types/timeline'; export type View = EventsViewType.tableView | EventsViewType.jsonView | EventsViewType.summaryView; export enum EventsViewType { @@ -29,6 +30,7 @@ interface Props { isAlert: boolean; view: EventsViewType; onViewSelected: (selected: EventsViewType) => void; + timelineTabType: TimelineTabs | 'flyout'; timelineId: string; } @@ -52,6 +54,7 @@ const EventDetailsComponent: React.FC = ({ id, view, onViewSelected, + timelineTabType, timelineId, isAlert, }) => { @@ -91,6 +94,7 @@ const EventDetailsComponent: React.FC = ({ data={data} eventId={id} timelineId={timelineId} + timelineTabType={timelineTabType} /> ), @@ -106,7 +110,7 @@ const EventDetailsComponent: React.FC = ({ ), }, ], - [alerts, browserFields, data, id, isAlert, timelineId] + [alerts, browserFields, data, id, isAlert, timelineId, timelineTabType] ); const selectedTab = useMemo(() => tabs.find((t) => t.id === view) ?? tabs[0], [tabs, view]); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx index cd50eb7880e56..0fc29e7193d4d 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx @@ -13,6 +13,7 @@ import { timelineActions } from '../../../timelines/store/timeline'; import { EventFieldsBrowser } from './event_fields_browser'; import { mockBrowserFields } from '../../containers/source/mock'; import { useMountAppended } from '../../utils/use_mount_appended'; +import { TimelineTabs } from '../../../../common/types/timeline'; jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); @@ -48,6 +49,7 @@ describe('EventFieldsBrowser', () => { data={mockDetailItemData} eventId={mockDetailItemDataId} timelineId="test" + timelineTabType={TimelineTabs.query} /> ); @@ -66,6 +68,7 @@ describe('EventFieldsBrowser', () => { data={mockDetailItemData} eventId={mockDetailItemDataId} timelineId="test" + timelineTabType={TimelineTabs.query} /> ); @@ -89,6 +92,7 @@ describe('EventFieldsBrowser', () => { data={mockDetailItemData} eventId={eventId} timelineId="test" + timelineTabType={TimelineTabs.query} /> ); @@ -108,6 +112,7 @@ describe('EventFieldsBrowser', () => { data={mockDetailItemData} eventId={eventId} timelineId="test" + timelineTabType={TimelineTabs.query} /> ); @@ -127,6 +132,7 @@ describe('EventFieldsBrowser', () => { data={mockDetailItemData} eventId={eventId} timelineId="test" + timelineTabType={TimelineTabs.query} /> ); @@ -158,6 +164,7 @@ describe('EventFieldsBrowser', () => { data={mockDetailItemData} eventId={mockDetailItemDataId} timelineId="test" + timelineTabType={TimelineTabs.query} /> ); @@ -182,6 +189,7 @@ describe('EventFieldsBrowser', () => { data={mockDetailItemData} eventId={mockDetailItemDataId} timelineId="test" + timelineTabType={TimelineTabs.query} /> ); @@ -196,6 +204,7 @@ describe('EventFieldsBrowser', () => { data={mockDetailItemData} eventId={mockDetailItemDataId} timelineId="test" + timelineTabType={TimelineTabs.query} /> ); @@ -220,6 +229,7 @@ describe('EventFieldsBrowser', () => { data={mockDetailItemData} eventId={mockDetailItemDataId} timelineId="test" + timelineTabType={TimelineTabs.query} /> ); @@ -238,6 +248,7 @@ describe('EventFieldsBrowser', () => { data={mockDetailItemData} eventId={mockDetailItemDataId} timelineId="test" + timelineTabType={TimelineTabs.query} /> ); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx index cd1579b299093..9733fafbe1c4d 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx @@ -29,12 +29,14 @@ import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { getColumns } from './columns'; import { EVENT_FIELDS_TABLE_CLASS_NAME, onEventDetailsTabKeyPressed, search } from './helpers'; import { useDeepEqualSelector } from '../../hooks/use_selector'; +import { TimelineTabs } from '../../../../common/types/timeline'; interface Props { browserFields: BrowserFields; data: TimelineEventsDetailsItem[]; eventId: string; timelineId: string; + timelineTabType: TimelineTabs | 'flyout'; } const TableWrapper = styled.div` @@ -87,7 +89,7 @@ const getAriaRowindex = (timelineEventsDetailsItem: TimelineEventsDetailsItem) = /** Renders a table view or JSON view of the `ECS` `data` */ export const EventFieldsBrowser = React.memo( - ({ browserFields, data, eventId, timelineId }) => { + ({ browserFields, data, eventId, timelineTabType, timelineId }) => { const containerElement = useRef(null); const dispatch = useDispatch(); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); @@ -156,7 +158,7 @@ export const EventFieldsBrowser = React.memo( columnHeaders, eventId, onUpdateColumns, - contextId: `event-fields-browser-for-${timelineId}`, + contextId: `event-fields-browser-for-${timelineId}-${timelineTabType}`, timelineId, toggleColumn, getLinkValue, @@ -167,6 +169,7 @@ export const EventFieldsBrowser = React.memo( eventId, onUpdateColumns, timelineId, + timelineTabType, toggleColumn, getLinkValue, ] diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_flyout.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_flyout.tsx index 48bdebbc0aa4f..9c09f2e696104 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_flyout.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_flyout.tsx @@ -39,7 +39,7 @@ const EventDetailsFlyoutComponent: React.FC = ({ const dispatch = useDispatch(); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const expandedEvent = useDeepEqualSelector( - (state) => (getTimeline(state, timelineId) ?? timelineDefaults)?.expandedEvent ?? {} + (state) => (getTimeline(state, timelineId) ?? timelineDefaults)?.expandedEvent?.query ?? {} ); const handleClearSelection = useCallback(() => { @@ -75,6 +75,7 @@ const EventDetailsFlyoutComponent: React.FC = ({ isAlert={isAlert} loading={loading} timelineId={timelineId} + timelineTabType="flyout" /> 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 423b3566e4eb5..6250345579cff 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 @@ -5,12 +5,13 @@ */ import React from 'react'; +import { waitFor, act } from '@testing-library/react'; import useResizeObserver from 'use-resize-observer/polyfilled'; import '../../mock/match_media'; import { mockIndexNames, mockIndexPattern, TestProviders } from '../../mock'; -import { mockEventViewerResponse } from './mock'; +import { mockEventViewerResponse, mockEventViewerResponseWithEvents } from './mock'; import { StatefulEventsViewer } from '.'; import { EventsViewer } from './events_viewer'; import { defaultHeaders } from './default_headers'; @@ -30,6 +31,15 @@ jest.mock('../../../timelines/components/graph_overlay', () => ({ GraphOverlay: jest.fn(() =>
), })); +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { @@ -50,6 +60,9 @@ const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; jest.mock('use-resize-observer/polyfilled'); mockUseResizeObserver.mockImplementation(() => ({})); +const mockUseTimelineEvents: jest.Mock = useTimelineEvents as jest.Mock; +jest.mock('../../../timelines/containers'); + const from = '2019-08-26T22:10:56.791Z'; const to = '2019-08-27T22:10:56.794Z'; @@ -108,14 +121,51 @@ describe('EventsViewer', () => { start: from, scopeId: SourcererScopeName.timeline, }; - beforeEach(() => { - (useTimelineEvents as jest.Mock).mockReturnValue([false, mockEventViewerResponse]); + mockUseTimelineEvents.mockReset(); }); beforeAll(() => { mockUseSourcererScope.mockImplementation(() => defaultMocks); }); + + describe('event details', () => { + beforeEach(() => { + mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponseWithEvents]); + }); + + test('call the right reduce action to show event details', async () => { + const wrapper = mount( + + + + ); + + await act(async () => { + wrapper.find(`[data-test-subj="expand-event"]`).first().simulate('click'); + }); + + await waitFor(() => { + expect(mockDispatch).toBeCalledTimes(2); + expect(mockDispatch.mock.calls[1][0]).toEqual({ + payload: { + event: { + eventId: 'yb8TkHYBRgU82_bJu_rY', + indexName: 'auditbeat-7.10.1-2020.12.18-000001', + }, + tabType: 'query', + timelineId: 'test-stateful-events-viewer', + }, + type: 'x-pack/security_solution/local/timeline/TOGGLE_EXPANDED_EVENT', + }); + }); + }); + }); + describe('rendering', () => { + beforeEach(() => { + mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]); + }); + test('it renders the "Showing..." subtitle with the expected event count', () => { const wrapper = mount( @@ -160,57 +210,66 @@ describe('EventsViewer', () => { ); }); }); - describe('loading', () => { - beforeAll(() => { - mockUseSourcererScope.mockImplementation(() => ({ ...defaultMocks, loading: true })); - }); - test('it does NOT render fetch index pattern is loading', () => { - const wrapper = mount( - - - - ); + }); - expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe( - false - ); - }); + describe('loading', () => { + beforeAll(() => { + mockUseSourcererScope.mockImplementation(() => ({ ...defaultMocks, loading: true })); + }); + beforeEach(() => { + mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]); + }); - test('it does NOT render when start is empty', () => { - testProps = { - ...testProps, - start: '', - }; - const wrapper = mount( - - - - ); + test('it does NOT render fetch index pattern is loading', () => { + const wrapper = mount( + + + + ); - expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe( - false - ); - }); + expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe( + false + ); + }); - test('it does NOT render when end is empty', () => { - testProps = { - ...testProps, - end: '', - }; - const wrapper = mount( - - - - ); + test('it does NOT render when start is empty', () => { + testProps = { + ...testProps, + start: '', + }; + const wrapper = mount( + + + + ); - expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe( - false - ); - }); + expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe( + false + ); + }); + + test('it does NOT render when end is empty', () => { + testProps = { + ...testProps, + end: '', + }; + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe( + false + ); }); }); describe('headerFilterGroup', () => { + beforeEach(() => { + mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]); + }); + test('it renders the provided headerFilterGroup', () => { const wrapper = mount( @@ -284,6 +343,10 @@ describe('EventsViewer', () => { }); describe('utilityBar', () => { + beforeEach(() => { + mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]); + }); + test('it renders the provided utilityBar when Resolver is NOT showing, because graphEventId is undefined', () => { const wrapper = mount( @@ -313,6 +376,10 @@ describe('EventsViewer', () => { }); describe('header inspect button', () => { + beforeEach(() => { + mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]); + }); + test('it renders the inspect button when Resolver is NOT showing, because graphEventId is undefined', () => { 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 7d38e3b732fc0..1d06f07bc774b 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 @@ -6,21 +6,15 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; -import React, { useEffect, useMemo, useState, useRef } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; -import { useDispatch } from 'react-redux'; import { Direction } from '../../../../common/search_strategy'; import { BrowserFields, DocValueFields } from '../../containers/source'; import { useTimelineEvents } from '../../../timelines/containers'; -import { timelineActions } from '../../../timelines/store/timeline'; import { useKibana } from '../../lib/kibana'; -import { - ColumnHeaderOptions, - KqlMode, - TimelineTabs, -} from '../../../timelines/store/timeline/model'; +import { ColumnHeaderOptions, KqlMode } from '../../../timelines/store/timeline/model'; import { HeaderSection } from '../header_section'; import { defaultHeaders } from '../../../timelines/components/timeline/body/column_headers/default_headers'; import { Sort } from '../../../timelines/components/timeline/body/sort'; @@ -45,7 +39,11 @@ import { inputsModel } from '../../store'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; import { ExitFullScreen } from '../exit_full_screen'; import { useGlobalFullScreen } from '../../containers/use_full_screen'; -import { TimelineExpandedEvent, TimelineId } from '../../../../common/types/timeline'; +import { + TimelineExpandedEventType, + TimelineId, + TimelineTabs, +} from '../../../../common/types/timeline'; import { GraphOverlay } from '../../../timelines/components/graph_overlay'; import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles'; @@ -114,7 +112,7 @@ interface Props { deletedEventIds: Readonly; docValueFields: DocValueFields[]; end: string; - expandedEvent: TimelineExpandedEvent; + expandedEvent: TimelineExpandedEventType; filters: Filter[]; headerFilterGroup?: React.ReactNode; height?: number; @@ -160,7 +158,6 @@ const EventsViewerComponent: React.FC = ({ utilityBar, graphEventId, }) => { - const dispatch = useDispatch(); const { globalFullScreen } = useGlobalFullScreen(); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; const kibana = useKibana(); @@ -191,9 +188,6 @@ const EventsViewerComponent: React.FC = ({ [justTitle] ); - const prevCombinedQueries = useRef<{ - filterQuery: string; - } | null>(null); const combinedQueries = combineQueries({ config: esQuery.getEsQueryConfig(kibana.services.uiSettings), dataProviders, @@ -220,12 +214,6 @@ const EventsViewerComponent: React.FC = ({ queryFields, ]); - const prevSortField = useRef< - Array<{ - field: string; - direction: Direction; - }> - >([]); const sortField = useMemo( () => sort.map(({ columnId, sortDirection }) => ({ @@ -251,17 +239,6 @@ const EventsViewerComponent: React.FC = ({ skip: !canQueryTimeline, }); - useEffect(() => { - if (!deepEqual(prevCombinedQueries.current, combinedQueries)) { - prevCombinedQueries.current = combinedQueries; - dispatch(timelineActions.toggleExpandedEvent({ timelineId: id })); - } - if (!deepEqual(prevSortField.current, sortField)) { - prevSortField.current = sortField; - dispatch(timelineActions.toggleExpandedEvent({ timelineId: id })); - } - }, [combinedQueries, dispatch, id, sortField]); - const totalCountMinusDeleted = useMemo( () => (totalCount > 0 ? totalCount - deletedEventIds.length : 0), [deletedEventIds.length, totalCount] 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 3272b0306f9c9..d7310ea776659 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 @@ -166,7 +166,7 @@ const makeMapStateToProps = () => { columns, dataProviders, deletedEventIds, - expandedEvent, + expandedEvent: expandedEvent?.query ?? {}, excludedRowRendererIds, filters: getGlobalFiltersQuerySelector(state), id, diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/mock.ts b/x-pack/plugins/security_solution/public/common/components/events_viewer/mock.ts index d2bd940dcc266..153992d9f1adb 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/mock.ts +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/mock.ts @@ -12,3 +12,91 @@ export const mockEventViewerResponse = { }, events: [], }; + +export const mockEventViewerResponseWithEvents = { + totalCount: 1, + pageInfo: { + activePage: 0, + fakeTotalCount: 100, + }, + events: [ + { + ecs: { + _id: 'yb8TkHYBRgU82_bJu_rY', + timestamp: '2020-12-23T14:49:39.957Z', + _index: 'auditbeat-7.10.1-2020.12.18-000001', + '@timestamp': ['2020-12-23T14:49:39.957Z'], + event: { + module: ['system'], + action: ['process_started'], + category: ['process'], + dataset: ['process'], + kind: ['event'], + type: ['start'], + }, + host: { + name: ['handsome'], + os: { + family: ['darwin'], + }, + id: ['33'], + ip: ['0.0.0.0'], + }, + user: { + name: ['handsome'], + }, + message: ['Process node (PID: 77895) by user handsome STARTED'], + agent: { + type: ['auditbeat'], + }, + process: { + hash: { + sha1: ['`12345678987654323456Y7U87654`'], + }, + pid: ['77895'], + name: ['node'], + ppid: ['73537'], + args: [ + '/Users/handsome/.nvm/versions/node/v14.15.3/bin/node', + '/Users/handsome/Documents/workspace/kibana/node_modules/jest-worker/build/workers/processChild.js', + ], + entity_id: ['3arNfOyR9NwR2u03'], + executable: ['/Users/handsome/.nvm/versions/node/v14.15.3/bin/node'], + working_directory: ['/Users/handsome/Documents/workspace/kibana/x-pack'], + }, + }, + data: [ + { + field: '@timestamp', + value: ['2020-12-23T14:49:39.957Z'], + }, + { + field: 'event.module', + value: ['system'], + }, + { + field: 'event.action', + value: ['process_started'], + }, + { + field: 'host.name', + value: ['handsome'], + }, + { + field: 'user.name', + value: ['handsome'], + }, + { + field: 'message', + value: ['Process node (PID: 77895) by user handsome STARTED'], + }, + { + field: 'event.dataset', + value: ['process'], + }, + ], + _id: 'yb8TkHYBRgU82_bJu_rY', + _index: 'auditbeat-7.10.1-2020.12.18-000001', + }, + ], +}; 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 36cdc807c4c0c..891e7bfffe868 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 @@ -11,7 +11,7 @@ import { HostsTableType } from '../../../../hosts/store/model'; import { RouteSpyState, SiemRouteType } from '../../../utils/route/types'; import { TabNavigationProps } from '../tab_navigation/types'; import { NetworkRouteType } from '../../../../network/pages/navigation/types'; -import { TimelineTabs } from '../../../../timelines/store/timeline/model'; +import { TimelineTabs } from '../../../../../common/types/timeline'; const setBreadcrumbsMock = jest.fn(); const chromeMock = { 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 158da3be3bbf7..f2fbe48c97c83 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 @@ -14,7 +14,7 @@ import { navTabs } from '../../../app/home/home_navigations'; import { HostsTableType } from '../../../hosts/store/model'; import { RouteSpyState } from '../../utils/route/types'; import { SiemNavigationProps, SiemNavigationComponentProps } from './types'; -import { TimelineTabs } from '../../../timelines/store/timeline/model'; +import { TimelineTabs } from '../../../../common/types/timeline'; jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); 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 f4ffc25146be5..e5c011cdc14be 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 @@ -6,12 +6,12 @@ import { mount } from 'enzyme'; import React from 'react'; +import { TimelineTabs } from '../../../../../common/types/timeline'; import { navTabs } from '../../../../app/home/home_navigations'; import { SecurityPageName } from '../../../../app/types'; import { navTabsHostDetails } from '../../../../hosts/pages/details/nav_tabs'; import { HostsTableType } from '../../../../hosts/store/model'; -import { TimelineTabs } from '../../../../timelines/store/timeline/model'; import { RouteSpyState } from '../../../utils/route/types'; import { CONSTANTS } from '../../url_state/constants'; import { TabNavigationComponent } from './'; diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap index a6aa844919709..a2a9c30ca4e1c 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap @@ -460,12 +460,7 @@ exports[`Paginated Table Component rendering it renders the default load more ta "euiRangeTrackHeight": "2px", "euiRangeTrackRadius": "4px", "euiRangeTrackWidth": "100%", - "euiResizableButtonSizeModifiers": Object { - "sizeExtraLarge": "40px", - "sizeLarge": "24px", - "sizeMedium": "16px", - "sizeSmall": "12px", - }, + "euiResizableButtonSize": "16px", "euiResizableButtonTransitionSpeed": "150ms", "euiScrollBar": "16px", "euiScrollBarCorner": "6px", diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts index 9932e52b6a1d1..9f51ecf9483b2 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts @@ -12,11 +12,11 @@ import * as H from 'history'; import { Query, Filter } from '../../../../../../../src/plugins/data/public'; import { url } from '../../../../../../../src/plugins/kibana_utils/public'; -import { TimelineId } from '../../../../common/types/timeline'; +import { TimelineId, TimelineTabs } from '../../../../common/types/timeline'; import { SecurityPageName } from '../../../app/types'; import { inputsSelectors, State } from '../../store'; import { UrlInputsModel } from '../../store/inputs/model'; -import { TimelineTabs, TimelineUrl } from '../../../timelines/store/timeline/model'; +import { TimelineUrl } from '../../../timelines/store/timeline/model'; import { timelineSelectors } from '../../../timelines/store/timeline'; import { formatDate } from '../super_date_picker'; import { NavTab } from '../navigation/types'; 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 bf5b6b1719605..d835636aa2778 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 @@ -17,7 +17,7 @@ import { Query } from '../../../../../../../src/plugins/data/public'; import { networkModel } from '../../../network/store'; import { hostsModel } from '../../../hosts/store'; import { HostsTableType } from '../../../hosts/store/model'; -import { TimelineTabs } from '../../../timelines/store/timeline/model'; +import { TimelineTabs } from '../../../../common/types/timeline'; type Action = 'PUSH' | 'POP' | 'REPLACE'; const pop: Action = 'POP'; 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 db21847991534..320c3a0736540 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 @@ -24,13 +24,12 @@ import { DEFAULT_INDEX_PATTERN, } from '../../../common/constants'; import { networkModel } from '../../network/store'; -import { TimelineType, TimelineStatus } from '../../../common/types/timeline'; +import { TimelineType, TimelineStatus, TimelineTabs } from '../../../common/types/timeline'; import { mockManagementState } from '../../management/store/reducer'; import { ManagementState } from '../../management/types'; import { initialSourcererState, SourcererScopeName } from '../store/sourcerer/model'; import { mockBrowserFields, mockDocValueFields } from '../containers/source/mock'; import { mockIndexPattern } from './index_pattern'; -import { TimelineTabs } from '../../timelines/store/timeline/model'; export const mockGlobalState: State = { app: { 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 c8d9fc981d880..03109803eb9d8 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 @@ -5,14 +5,19 @@ */ import { FilterStateStore } from '../../../../../../src/plugins/data/common/es_query/filters/meta_filter'; -import { TimelineId, TimelineType, TimelineStatus } from '../../../common/types/timeline'; +import { + TimelineId, + TimelineType, + TimelineStatus, + TimelineTabs, +} from '../../../common/types/timeline'; import { OpenTimelineResult } from '../../timelines/components/open_timeline/types'; import { GetAllTimeline, SortFieldTimeline, TimelineResult, Direction } from '../../graphql/types'; import { TimelineEventsDetailsItem } from '../../../common/search_strategy'; import { allTimelinesQuery } from '../../timelines/containers/all/index.gql_query'; import { CreateTimelineProps } from '../../detections/components/alerts_table/types'; -import { TimelineModel, TimelineTabs } from '../../timelines/store/timeline/model'; +import { TimelineModel } from '../../timelines/store/timeline/model'; import { timelineDefaults } from '../../timelines/store/timeline/defaults'; export interface MockedProvidedQuery { 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 d251cce381536..64e916f87b09d 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 @@ -19,10 +19,14 @@ import { } from '../../../common/mock/'; import { CreateTimeline, UpdateTimelineLoading } from './types'; import { Ecs } from '../../../../common/ecs'; -import { TimelineId, TimelineType, TimelineStatus } from '../../../../common/types/timeline'; +import { + TimelineId, + TimelineType, + TimelineStatus, + TimelineTabs, +} from '../../../../common/types/timeline'; import { ISearchStart } from '../../../../../../../src/plugins/data/public'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; -import { TimelineTabs } from '../../../timelines/store/timeline/model'; jest.mock('apollo-client'); diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx index f0e47fcd5c104..57c4eee95cd8c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx @@ -32,8 +32,8 @@ import { useAppToasts } from '../../../common/hooks/use_app_toasts'; import * as i18n from './translations'; import { buildColumns } from './table_helpers'; import { ValueListsForm } from './form'; -import { AutoDownload } from './auto_download'; import { ReferenceErrorModal } from './reference_error_modal'; +import { AutoDownload } from '../../../common/components/auto_download/auto_download'; interface ValueListsModalProps { onClose: () => void; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx index 57b86119dc164..79cfd53a4fa00 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { EuiButtonIcon, EuiBasicTableColumn, EuiToolTip } from '@elastic/eui'; import { History } from 'history'; +import { NamespaceType } from '../../../../../../../../lists/common'; import { FormatUrl } from '../../../../../../common/components/link_to'; import { LinkAnchor } from '../../../../../../common/components/links'; import * as i18n from './translations'; @@ -16,7 +17,11 @@ import { ExceptionListInfo } from './use_all_exception_lists'; import { getRuleDetailsUrl } from '../../../../../../common/components/link_to/redirect_to_detection_engine'; export type AllExceptionListsColumns = EuiBasicTableColumn; -export type Func = (listId: string) => () => void; +export type Func = (arg: { + id: string; + listId: string; + namespaceType: NamespaceType; +}) => () => void; export const getAllExceptionListsColumns = ( onExport: Func, @@ -96,9 +101,13 @@ export const getAllExceptionListsColumns = ( align: 'center', isExpander: false, width: '25px', - render: (list: ExceptionListInfo) => ( + render: ({ id, list_id: listId, namespace_type: namespaceType }: ExceptionListInfo) => ( @@ -108,10 +117,14 @@ export const getAllExceptionListsColumns = ( align: 'center', width: '25px', isExpander: false, - render: (list: ExceptionListInfo) => ( + render: ({ id, list_id: listId, namespace_type: namespaceType }: ExceptionListInfo) => ( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx index 65aaaea06b40f..ac9c558022c26 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo, useEffect, useCallback, useState, ChangeEvent } from 'react'; +import React, { useMemo, useEffect, useCallback, useState } from 'react'; import { EuiBasicTable, EuiEmptyPrompt, @@ -16,8 +16,10 @@ import styled from 'styled-components'; import { History } from 'history'; import { set } from 'lodash/fp'; +import { AutoDownload } from '../../../../../../common/components/auto_download/auto_download'; +import { NamespaceType } from '../../../../../../../../lists/common'; import { useKibana } from '../../../../../../common/lib/kibana'; -import { useExceptionLists } from '../../../../../../shared_imports'; +import { useApi, useExceptionLists } from '../../../../../../shared_imports'; import { FormatUrl } from '../../../../../../common/components/link_to'; import { HeaderSection } from '../../../../../../common/components/header_section'; import { Loader } from '../../../../../../common/components/loader'; @@ -51,6 +53,7 @@ export const ExceptionListsTable = React.memo( const { services: { http, notifications }, } = useKibana(); + const { exportExceptionList } = useApi(http); const [filters, setFilters] = useState({ name: null, list_id: null, @@ -69,10 +72,67 @@ export const ExceptionListsTable = React.memo( }); const [initLoading, setInitLoading] = useState(true); const [lastUpdated, setLastUpdated] = useState(Date.now()); + const [deletingListIds, setDeletingListIds] = useState([]); + const [exportingListIds, setExportingListIds] = useState([]); + const [exportDownload, setExportDownload] = useState<{ name?: string; blob?: Blob }>({}); - const handleDelete = useCallback((id: string) => () => {}, []); + const handleDelete = useCallback( + ({ + id, + listId, + namespaceType, + }: { + id: string; + listId: string; + namespaceType: NamespaceType; + }) => async () => { + try { + setDeletingListIds((ids) => [...ids, id]); + // route to patch rules with associated exception list + } catch (error) { + notifications.toasts.addError(error, { title: i18n.EXCEPTION_DELETE_ERROR }); + } finally { + setDeletingListIds((ids) => [...ids.filter((_id) => _id !== id)]); + } + }, + [notifications.toasts] + ); - const handleExport = useCallback((id: string) => () => {}, []); + const handleExportSuccess = useCallback( + (listId: string) => (blob: Blob): void => { + setExportDownload({ name: listId, blob }); + }, + [] + ); + + const handleExportError = useCallback( + (err: Error) => { + notifications.toasts.addError(err, { title: i18n.EXCEPTION_EXPORT_ERROR }); + }, + [notifications.toasts] + ); + + const handleExport = useCallback( + ({ + id, + listId, + namespaceType, + }: { + id: string; + listId: string; + namespaceType: NamespaceType; + }) => async () => { + setExportingListIds((ids) => [...ids, id]); + await exportExceptionList({ + id, + listId, + namespaceType, + onError: handleExportError, + onSuccess: handleExportSuccess(listId), + }); + }, + [exportExceptionList, handleExportError, handleExportSuccess] + ); const exceptionsColumns = useMemo((): AllExceptionListsColumns[] => { return getAllExceptionListsColumns(handleExport, handleDelete, history, formatUrl); @@ -122,14 +182,6 @@ export const ExceptionListsTable = React.memo( setFilters(formattedFilter); }, []); - const handleSearchChange = useCallback( - (event: ChangeEvent) => { - const val = event.target.value; - handleSearch(val); - }, - [handleSearch] - ); - const paginationMemo = useMemo( () => ({ pageIndex: pagination.page - 1, @@ -140,8 +192,23 @@ export const ExceptionListsTable = React.memo( [pagination] ); + const handleOnDownload = useCallback(() => { + setExportDownload({}); + }, []); + + const tableItems = (data ?? []).map((item) => ({ + ...item, + isDeleting: deletingListIds.includes(item.id), + isExporting: exportingListIds.includes(item.id), + })); + return ( <> + <> {loadingTableInfo && ( @@ -162,7 +229,6 @@ export const ExceptionListsTable = React.memo( aria-label={i18n.EXCEPTIONS_LISTS_SEARCH_PLACEHOLDER} placeholder={i18n.EXCEPTIONS_LISTS_SEARCH_PLACEHOLDER} onSearch={handleSearch} - onChange={handleSearchChange} disabled={initLoading} incremental={false} fullWidth @@ -188,7 +254,7 @@ export const ExceptionListsTable = React.memo( columns={exceptionsColumns} isSelectable={!hasNoPermissions ?? false} itemId="id" - items={data ?? []} + items={tableItems} noItemsMessage={emptyPrompt} onChange={() => {}} pagination={paginationMemo} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/translations.ts index 2eba8fb2e579b..7483b8e943d30 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/translations.ts @@ -35,7 +35,7 @@ export const LIST_DATE_CREATED_TITLE = i18n.translate( ); export const LIST_DATE_UPDATED_TITLE = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.all.exceptions.dateUPdatedTitle', + 'xpack.securitySolution.detectionEngine.rules.all.exceptions.dateUpdatedTitle', { defaultMessage: 'Last edited', } @@ -75,3 +75,24 @@ export const NO_LISTS_BODY = i18n.translate( defaultMessage: "We weren't able to find any exception lists.", } ); + +export const EXCEPTION_EXPORT_SUCCESS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.all.exceptions.exportSuccess', + { + defaultMessage: 'Exception list export success', + } +); + +export const EXCEPTION_EXPORT_ERROR = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.all.exceptions.exportError', + { + defaultMessage: 'Exception list export error', + } +); + +export const EXCEPTION_DELETE_ERROR = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.all.exceptions.deleteError', + { + defaultMessage: 'Error occurred deleting exception list', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/use_all_exception_lists.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/use_all_exception_lists.tsx index 4b47080cc2da1..3f343da605213 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/use_all_exception_lists.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/use_all_exception_lists.tsx @@ -61,7 +61,7 @@ export const useAllExceptionLists = ({ const { data: rules } = await fetchRules({ pagination: { page: 1, - perPage: 500, + perPage: 10000, total: 0, }, signal: abortCtrl.signal, diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.test.tsx index c9bd1ee13cd95..f9bb8b58634fa 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.test.tsx @@ -8,7 +8,7 @@ import { mount } from 'enzyme'; import React from 'react'; import { TestProviders } from '../../../../common/mock/test_providers'; -import { TimelineTabs } from '../../../store/timeline/model'; +import { TimelineTabs } from '../../../../../common/types/timeline'; import { FlyoutBottomBar } from '.'; describe('FlyoutBottomBar', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.tsx index e6de34f1bf7a4..edc571528e94a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.tsx @@ -14,7 +14,7 @@ import { DataProvider } from '../../timeline/data_providers/data_provider'; import { flattenIntoAndGroups } from '../../timeline/data_providers/helpers'; import { DataProviders } from '../../timeline/data_providers'; import { FlyoutHeaderPanel } from '../header'; -import { TimelineTabs } from '../../../store/timeline/model'; +import { TimelineTabs } from '../../../../../common/types/timeline'; export const FLYOUT_BUTTON_CLASS_NAME = 'timeline-flyout-button'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx index e22a6616ecfc6..73c2eae1402c0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx @@ -20,7 +20,7 @@ import styled from 'styled-components'; import { FormattedRelative } from '@kbn/i18n/react'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; -import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline'; +import { TimelineStatus, TimelineTabs, TimelineType } from '../../../../../common/types/timeline'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; import { timelineDefaults } from '../../../../timelines/store/timeline/defaults'; import { AddToFavoritesButton } from '../../timeline/properties/helpers'; @@ -33,7 +33,6 @@ import { ActiveTimelines } from './active_timelines'; import * as i18n from './translations'; import * as commonI18n from '../../timeline/properties/translations'; import { getTimelineStatusByIdSelector } from './selectors'; -import { TimelineTabs } from '../../../store/timeline/model'; // to hide side borders const StyledPanel = styled(EuiPanel)` diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx index 622efefc6230a..6881ad3ee4bc1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx @@ -11,10 +11,9 @@ import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import { AppLeaveHandler } from '../../../../../../../src/core/public'; -import { TimelineId, TimelineStatus } from '../../../../common/types/timeline'; +import { TimelineId, TimelineStatus, TimelineTabs } from '../../../../common/types/timeline'; import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { timelineActions } from '../../store/timeline'; -import { TimelineTabs } from '../../store/timeline/model'; import { FlyoutBottomBar } from './bottom_bar'; import { Pane } from './pane'; import { getTimelineShowStatusByIdSelector } from './selectors'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/selectors.ts b/x-pack/plugins/security_solution/public/timelines/components/flyout/selectors.ts index 0ec4fecedfa7f..e6892c121ed44 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/selectors.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/selectors.ts @@ -6,9 +6,8 @@ import { createSelector } from 'reselect'; -import { TimelineStatus } from '../../../../common/types/timeline'; +import { TimelineStatus, TimelineTabs } from '../../../../common/types/timeline'; import { timelineSelectors } from '../../store/timeline'; -import { TimelineTabs } from '../../store/timeline/model'; export const getTimelineShowStatusByIdSelector = () => createSelector(timelineSelectors.selectTimeline, (timeline) => ({ 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 61b0c004dcb9d..da6eec968d11c 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,12 +39,16 @@ import { KueryFilterQueryKind } from '../../../common/store/model'; import { Note } from '../../../common/lib/note'; import moment from 'moment'; import sinon from 'sinon'; -import { TimelineId, TimelineType, TimelineStatus } from '../../../../common/types/timeline'; +import { + TimelineId, + TimelineType, + TimelineStatus, + TimelineTabs, +} from '../../../../common/types/timeline'; import { mockTimeline as mockSelectedTimeline, mockTemplate as mockSelectedTemplate, } from './__mocks__'; -import { TimelineTabs } from '../../store/timeline/model'; jest.mock('../../../common/store/inputs/actions'); jest.mock('../../../common/components/url_state/normalize_time_range.ts'); 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 37de75fd736af..c7821df347311 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 @@ -27,6 +27,7 @@ import { TimelineId, TimelineStatus, TimelineType, + TimelineTabs, } from '../../../../common/types/timeline'; import { @@ -42,11 +43,7 @@ import { addTimeline as dispatchAddTimeline, addNote as dispatchAddGlobalTimelineNote, } from '../../../timelines/store/timeline/actions'; -import { - ColumnHeaderOptions, - TimelineModel, - TimelineTabs, -} from '../../../timelines/store/timeline/model'; +import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx index fc05e61442e83..d35a5f487ed8e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx @@ -17,6 +17,7 @@ import { MarkdownRenderer } from '../../../../common/components/markdown_editor' import { timelineActions } from '../../../store/timeline'; import { NOTE_CONTENT_CLASS_NAME } from '../../timeline/body/helpers'; import * as i18n from './translations'; +import { TimelineTabs } from '../../../../../common/types/timeline'; export const NotePreviewsContainer = styled.section` padding-top: ${({ theme }) => `${theme.eui.euiSizeS}`}; @@ -37,6 +38,7 @@ const ToggleEventDetailsButtonComponent: React.FC const handleClick = useCallback(() => { dispatch( timelineActions.toggleExpandedEvent({ + tabType: TimelineTabs.notes, timelineId, event: { eventId, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx index 0242a2a0ff091..21ca30658f530 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx @@ -11,7 +11,8 @@ import { getOr } from 'lodash/fp'; import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '../../../../../common/components/drag_and_drop/helpers'; import { Ecs } from '../../../../../../common/ecs'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; -import { ColumnHeaderOptions, TimelineTabs } from '../../../../../timelines/store/timeline/model'; +import { TimelineTabs } from '../../../../../../common/types/timeline'; +import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers'; import { EventsTd, EVENTS_TD_CLASS_NAME, EventsTdContent, EventsTdGroupData } from '../../styles'; import { ColumnRenderer } from '../renderers/column_renderer'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx index 0525767e616be..cff3d2890d85a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx @@ -11,7 +11,7 @@ import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants'; import * as i18n from '../translations'; import { EventColumnView } from './event_column_view'; -import { TimelineType } from '../../../../../../common/types/timeline'; +import { TimelineTabs, TimelineType } from '../../../../../../common/types/timeline'; import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; jest.mock('../../../../../common/hooks/use_selector'); @@ -48,6 +48,7 @@ describe('EventColumnView', () => { selectedEventIds: {}, showCheckboxes: false, showNotes: false, + tabType: TimelineTabs.query, timelineId: 'timeline-test', toggleShowNotes: jest.fn(), updateNote: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx index ae8d2a47c7dc7..4e61fb7346c5c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx @@ -9,7 +9,7 @@ import React, { useCallback, useMemo } from 'react'; import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; import { Ecs } from '../../../../../../common/ecs'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; -import { ColumnHeaderOptions, TimelineTabs } from '../../../../../timelines/store/timeline/model'; +import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events'; import { EventsTrData } from '../../styles'; import { Actions } from '../actions'; @@ -26,7 +26,7 @@ import { InvestigateInTimelineAction } from '../../../../../detections/component import { AddEventNoteAction } from '../actions/add_note_icon_item'; import { PinEventAction } from '../actions/pin_event_action'; import { inputsModel } from '../../../../../common/store'; -import { TimelineId } from '../../../../../../common/types/timeline'; +import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline'; import { timelineSelectors } from '../../../../store/timeline'; import { timelineDefaults } from '../../../../store/timeline/defaults'; import { AddToCaseAction } from '../../../../../cases/components/timeline_actions/add_to_case_action'; 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 92ae01b185f7a..dba08823b87fe 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 @@ -12,7 +12,8 @@ import { TimelineItem, TimelineNonEcsData, } from '../../../../../../common/search_strategy/timeline'; -import { ColumnHeaderOptions, TimelineTabs } from '../../../../../timelines/store/timeline/model'; +import { TimelineTabs } from '../../../../../../common/types/timeline'; +import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { OnRowSelected } from '../../events'; import { EventsTbody } from '../../styles'; import { ColumnRenderer } from '../renderers/column_renderer'; 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 e3f5a744e8b7d..f00b86ef96567 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 @@ -8,13 +8,13 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; -import { TimelineId } from '../../../../../../common/types/timeline'; +import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline'; import { BrowserFields } from '../../../../../common/containers/source'; import { TimelineItem, TimelineNonEcsData, } from '../../../../../../common/search_strategy/timeline'; -import { ColumnHeaderOptions, TimelineTabs } from '../../../../../timelines/store/timeline/model'; +import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { OnPinEvent, OnRowSelected } from '../../events'; import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../helpers'; import { EventsTrGroup, EventsTrSupplement, EventsTrSupplementContainer } from '../../styles'; @@ -92,7 +92,10 @@ const StatefulEventComponent: React.FC = ({ const [showNotes, setShowNotes] = useState<{ [eventId: string]: boolean }>({}); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const expandedEvent = useDeepEqualSelector( - (state) => (getTimeline(state, timelineId) ?? timelineDefaults).expandedEvent + (state) => + (getTimeline(state, timelineId) ?? timelineDefaults).expandedEvent[ + tabType ?? TimelineTabs.query + ] ?? {} ); const getNotesByIds = useMemo(() => appSelectors.notesByIdsSelector(), []); const notesById = useDeepEqualSelector(getNotesByIds); @@ -153,6 +156,7 @@ const StatefulEventComponent: React.FC = ({ dispatch( timelineActions.toggleExpandedEvent({ + tabType, timelineId, event: { eventId, @@ -161,10 +165,10 @@ const StatefulEventComponent: React.FC = ({ }) ); - if (timelineId === TimelineId.active) { + if (timelineId === TimelineId.active && tabType === TimelineTabs.query) { activeTimeline.toggleExpandedEvent({ eventId, indexName }); } - }, [dispatch, event._id, event._index, timelineId]); + }, [dispatch, event._id, event._index, tabType, timelineId]); const associateNote = useCallback( (noteId: string) => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx index 0295d44b646d7..3a738db981b38 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx @@ -16,12 +16,11 @@ import { TimelineTypeLiteral, TimelineType, TimelineId, + TimelineTabs, } from '../../../../../common/types/timeline'; import { OnPinEvent, OnUnPinEvent } from '../events'; import { ActionIconItem } from './actions/action_icon_item'; - import * as i18n from './translations'; -import { TimelineTabs } from '../../../store/timeline/model'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const omitTypenameAndEmpty = (k: string, v: any): any | undefined => 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 c8e911db85f64..cc04b83382998 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 @@ -17,7 +17,7 @@ import { BodyComponent, StatefulBodyProps } from '.'; import { Sort } from './sort'; import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { timelineActions } from '../../../store/timeline'; -import { TimelineTabs } from '../../../store/timeline/model'; +import { TimelineTabs } from '../../../../../common/types/timeline'; const mockSort: Sort[] = [ { @@ -221,4 +221,78 @@ describe('Body', () => { ); }); }); + + describe('event details', () => { + beforeEach(() => { + mockDispatch.mockReset(); + }); + test('call the right reduce action to show event details for query tab', async () => { + const wrapper = mount( + + + + ); + + wrapper.find(`[data-test-subj="expand-event"]`).first().simulate('click'); + wrapper.update(); + expect(mockDispatch).toBeCalledTimes(1); + expect(mockDispatch.mock.calls[0][0]).toEqual({ + payload: { + event: { + eventId: '1', + indexName: undefined, + }, + tabType: 'query', + timelineId: 'timeline-test', + }, + type: 'x-pack/security_solution/local/timeline/TOGGLE_EXPANDED_EVENT', + }); + }); + + test('call the right reduce action to show event details for pinned tab', async () => { + const wrapper = mount( + + + + ); + + wrapper.find(`[data-test-subj="expand-event"]`).first().simulate('click'); + wrapper.update(); + expect(mockDispatch).toBeCalledTimes(1); + expect(mockDispatch.mock.calls[0][0]).toEqual({ + payload: { + event: { + eventId: '1', + indexName: undefined, + }, + tabType: 'pinned', + timelineId: 'timeline-test', + }, + type: 'x-pack/security_solution/local/timeline/TOGGLE_EXPANDED_EVENT', + }); + }); + + test('call the right reduce action to show event details for notes tab', async () => { + const wrapper = mount( + + + + ); + + wrapper.find(`[data-test-subj="expand-event"]`).first().simulate('click'); + wrapper.update(); + expect(mockDispatch).toBeCalledTimes(1); + expect(mockDispatch.mock.calls[0][0]).toEqual({ + payload: { + event: { + eventId: '1', + indexName: undefined, + }, + tabType: 'notes', + timelineId: 'timeline-test', + }, + type: 'x-pack/security_solution/local/timeline/TOGGLE_EXPANDED_EVENT', + }); + }); + }); }); 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 4a33d0d3af33e..a03f4c07645ad 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 @@ -10,7 +10,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; -import { RowRendererId, TimelineId } from '../../../../../common/types/timeline'; +import { RowRendererId, TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; import { FIRST_ARIA_INDEX, ARIA_COLINDEX_ATTRIBUTE, @@ -21,7 +21,7 @@ import { BrowserFields } from '../../../../common/containers/source'; import { TimelineItem } from '../../../../../common/search_strategy/timeline'; import { inputsModel, State } from '../../../../common/store'; import { useManageTimeline } from '../../manage_timeline'; -import { ColumnHeaderOptions, TimelineModel, TimelineTabs } from '../../../store/timeline/model'; +import { ColumnHeaderOptions, TimelineModel } from '../../../store/timeline/model'; import { timelineDefaults } from '../../../store/timeline/defaults'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; import { OnRowSelected, OnSelectAll } from '../events'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/event_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/event_details.tsx index 9895f4eda0e6c..c75f8a0d1c170 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/event_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/event_details.tsx @@ -25,10 +25,12 @@ import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { useTimelineEventsDetails } from '../../containers/details'; import { timelineSelectors } from '../../store/timeline'; import { timelineDefaults } from '../../store/timeline/defaults'; +import { TimelineTabs } from '../../../../common/types/timeline'; interface EventDetailsProps { browserFields: BrowserFields; docValueFields: DocValueFields[]; + tabType: TimelineTabs; timelineId: string; handleOnEventClosed?: HandleOnEventClosed; } @@ -36,12 +38,13 @@ interface EventDetailsProps { const EventDetailsComponent: React.FC = ({ browserFields, docValueFields, + tabType, timelineId, handleOnEventClosed, }) => { const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const expandedEvent = useDeepEqualSelector( - (state) => (getTimeline(state, timelineId) ?? timelineDefaults).expandedEvent + (state) => (getTimeline(state, timelineId) ?? timelineDefaults).expandedEvent[tabType] ?? {} ); const [loading, detailsData] = useTimelineEventsDetails({ @@ -71,6 +74,7 @@ const EventDetailsComponent: React.FC = ({ isAlert={isAlert} loading={loading} timelineId={timelineId} + timelineTabType={tabType} /> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx index df8e84b4e2a78..a38fde0e3f548 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx @@ -20,7 +20,7 @@ import { import React, { useMemo, useState } from 'react'; import styled from 'styled-components'; -import { TimelineExpandedEvent } from '../../../../../common/types/timeline'; +import { TimelineExpandedEventType, TimelineTabs } from '../../../../../common/types/timeline'; import { BrowserFields } from '../../../../common/containers/source'; import { EventDetails, @@ -35,9 +35,10 @@ export type HandleOnEventClosed = () => void; interface Props { browserFields: BrowserFields; detailsData: TimelineEventsDetailsItem[] | null; - event: TimelineExpandedEvent; + event: TimelineExpandedEventType; isAlert: boolean; loading: boolean; + timelineTabType: TimelineTabs | 'flyout'; timelineId: string; } @@ -71,7 +72,7 @@ export const ExpandableEventTitle = React.memo( ExpandableEventTitle.displayName = 'ExpandableEventTitle'; export const ExpandableEvent = React.memo( - ({ browserFields, event, timelineId, isAlert, loading, detailsData }) => { + ({ browserFields, event, timelineId, timelineTabType, isAlert, loading, detailsData }) => { const [view, setView] = useState(EventsViewType.summaryView); const message = useMemo(() => { @@ -116,6 +117,7 @@ export const ExpandableEvent = React.memo( id={event.eventId!} isAlert={isAlert} onViewSelected={setView} + timelineTabType={timelineTabType} timelineId={timelineId} view={view} /> 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 41ac16a12e648..2b26e3f9eb0b5 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 @@ -17,7 +17,7 @@ import { isTab } from '../../../common/components/accessibility/helpers'; import { useSourcererScope } from '../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { FlyoutHeader, FlyoutHeaderPanel } from '../flyout/header'; -import { TimelineType } from '../../../../common/types/timeline'; +import { TimelineType, TimelineTabs } from '../../../../common/types/timeline'; import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector'; import { activeTimeline } from '../../containers/active_timeline_context'; import { EVENTS_COUNT_BUTTON_CLASS_NAME, onTimelineTabKeyPressed } from './helpers'; @@ -68,7 +68,9 @@ const StatefulTimelineComponent: React.FC = ({ timelineId }) => { id: timelineId, columns: defaultHeaders, indexNames: selectedPatterns, - expandedEvent: activeTimeline.getExpandedEvent(), + expandedEvent: { + [TimelineTabs.query]: activeTimeline.getExpandedEvent(), + }, show: false, }) ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx index bfb990cbd7364..34e5aed885d5c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { filter, pick, uniqBy } from 'lodash/fp'; +import { filter, uniqBy } from 'lodash/fp'; import { EuiAvatar, EuiFlexGroup, @@ -21,17 +21,17 @@ import styled from 'styled-components'; import { useSourcererScope } from '../../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; -import { timelineActions, timelineSelectors } from '../../../store/timeline'; +import { timelineActions } from '../../../store/timeline'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; -import { TimelineStatus } from '../../../../../common/types/timeline'; +import { TimelineStatus, TimelineTabs } from '../../../../../common/types/timeline'; import { appSelectors } from '../../../../common/store/app'; -import { timelineDefaults } from '../../../store/timeline/defaults'; import { AddNote } from '../../notes/add_note'; import { CREATED_BY, NOTES } from '../../notes/translations'; import { PARTICIPANTS } from '../../../../cases/translations'; import { NotePreviews } from '../../open_timeline/note_previews'; import { TimelineResultNote } from '../../open_timeline/types'; import { EventDetails } from '../event_details'; +import { getTimelineNoteSelector } from './selectors'; const FullWidthFlexGroup = styled(EuiFlexGroup)` width: 100%; @@ -121,18 +121,14 @@ interface NotesTabContentProps { const NotesTabContentComponent: React.FC = ({ timelineId }) => { const dispatch = useDispatch(); - const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const getTimelineNotes = useMemo(() => getTimelineNoteSelector(), []); const { createdBy, expandedEvent, eventIdToNoteIds, + noteIds, status: timelineStatus, - } = useDeepEqualSelector((state) => - pick( - ['createdBy', 'expandedEvent', 'eventIdToNoteIds', 'status'], - getTimeline(state, timelineId) ?? timelineDefaults - ) - ); + } = useDeepEqualSelector((state) => getTimelineNotes(state, timelineId)); const { browserFields, docValueFields } = useSourcererScope(SourcererScopeName.timeline); @@ -142,7 +138,20 @@ const NotesTabContentComponent: React.FC = ({ timelineId } ); const [newNote, setNewNote] = useState(''); const isImmutable = timelineStatus === TimelineStatus.immutable; - const notes: TimelineResultNote[] = useDeepEqualSelector(getNotesAsCommentsList); + const appNotes: TimelineResultNote[] = useDeepEqualSelector(getNotesAsCommentsList); + + const allTimelineNoteIds = useMemo(() => { + const eventNoteIds = Object.values(eventIdToNoteIds).reduce( + (acc, v) => [...acc, ...v], + [] + ); + return [...noteIds, ...eventNoteIds]; + }, [noteIds, eventIdToNoteIds]); + + const notes = useMemo( + () => appNotes.filter((appNote) => allTimelineNoteIds.includes(appNote?.noteId ?? '-1')), + [appNotes, allTimelineNoteIds] + ); // filter for savedObjectId to make sure we don't display `elastic` user while saving the note const participants = useMemo(() => uniqBy('updatedBy', filter('savedObjectId', notes)), [notes]); @@ -153,20 +162,21 @@ const NotesTabContentComponent: React.FC = ({ timelineId } ); const handleOnEventClosed = useCallback(() => { - dispatch(timelineActions.toggleExpandedEvent({ timelineId })); + dispatch(timelineActions.toggleExpandedEvent({ tabType: TimelineTabs.notes, timelineId })); }, [dispatch, timelineId]); const EventDetailsContent = useMemo( () => - expandedEvent.eventId ? ( + expandedEvent?.eventId != null ? ( ) : null, - [browserFields, docValueFields, expandedEvent.eventId, handleOnEventClosed, timelineId] + [browserFields, docValueFields, expandedEvent, handleOnEventClosed, timelineId] ); const SidebarContent = useMemo( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/selectors.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/selectors.ts new file mode 100644 index 0000000000000..37ee980b1a4ae --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/selectors.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 { createSelector } from 'reselect'; + +import { timelineSelectors } from '../../../store/timeline'; + +export const getTimelineNoteSelector = () => + createSelector(timelineSelectors.selectTimeline, (timeline) => { + return { + createdBy: timeline.createdBy, + expandedEvent: timeline.expandedEvent?.notes ?? {}, + eventIdToNoteIds: timeline?.eventIdToNoteIds ?? {}, + noteIds: timeline.noteIds, + status: timeline.status, + }; + }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx index a0d2ca57f90b3..1054b5405d9d9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx @@ -23,11 +23,12 @@ import { EventDetailsWidthProvider } from '../../../../common/components/events_ import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; import { timelineDefaults } from '../../../store/timeline/defaults'; import { useSourcererScope } from '../../../../common/containers/sourcerer'; -import { TimelineModel, TimelineTabs } from '../../../store/timeline/model'; +import { TimelineModel } from '../../../store/timeline/model'; import { EventDetails } from '../event_details'; import { ToggleExpandedEvent } from '../../../store/timeline/actions'; import { State } from '../../../../common/store'; import { calculateTotalPages } from '../helpers'; +import { TimelineTabs } from '../../../../../common/types/timeline'; const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` overflow-y: hidden; @@ -167,7 +168,7 @@ export const PinnedTabContentComponent: React.FC = ({ }); const handleOnEventClosed = useCallback(() => { - onEventClosed({ timelineId }); + onEventClosed({ tabType: TimelineTabs.pinned, timelineId }); }, [timelineId, onEventClosed]); return ( @@ -218,6 +219,7 @@ export const PinnedTabContentComponent: React.FC = ({ @@ -248,7 +250,7 @@ const makeMapStateToProps = () => { itemsPerPage, itemsPerPageOptions, pinnedEventIds, - showEventDetails: !!expandedEvent.eventId, + showEventDetails: !!expandedEvent[TimelineTabs.pinned]?.eventId, sort, }; }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx index 4769c826a2fad..b24a4afcbeea2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx @@ -17,12 +17,11 @@ import { QueryTabContentComponent, Props as QueryTabContentComponentProps } from import { Sort } from '../body/sort'; import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; import { useMountAppended } from '../../../../common/utils/use_mount_appended'; -import { TimelineId, TimelineStatus } from '../../../../../common/types/timeline'; +import { TimelineId, TimelineStatus, TimelineTabs } from '../../../../../common/types/timeline'; import { useTimelineEvents } from '../../../containers/index'; import { useTimelineEventsDetails } from '../../../containers/details/index'; import { useSourcererScope } from '../../../../common/containers/sourcerer'; import { mockSourcererScope } from '../../../../common/containers/sourcerer/mocks'; -import { TimelineTabs } from '../../../store/timeline/model'; jest.mock('../../../containers/index', () => ({ useTimelineEvents: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx index c0840d58174b3..d4c03117adcb9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx @@ -14,7 +14,7 @@ import { EuiBadge, } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; -import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react'; +import React, { useState, useMemo, useEffect, useCallback } from 'react'; import styled from 'styled-components'; import { Dispatch } from 'redux'; import { connect, ConnectedProps } from 'react-redux'; @@ -33,7 +33,7 @@ import { calculateTotalPages, combineQueries } from '../helpers'; import { TimelineRefetch } from '../refetch_timeline'; import { esQuery, FilterManager } from '../../../../../../../../src/plugins/data/public'; import { useManageTimeline } from '../../manage_timeline'; -import { TimelineEventsType, TimelineId } from '../../../../../common/types/timeline'; +import { TimelineEventsType, TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; import { requiredFieldsForActions } from '../../../../detections/components/alerts_table/default_config'; import { SuperDatePicker } from '../../../../common/components/super_date_picker'; import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context'; @@ -44,7 +44,7 @@ import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; import { timelineDefaults } from '../../../../timelines/store/timeline/defaults'; import { useSourcererScope } from '../../../../common/containers/sourcerer'; import { useTimelineEventsCountPortal } from '../../../../common/hooks/use_timeline_events_count'; -import { TimelineModel, TimelineTabs } from '../../../../timelines/store/timeline/model'; +import { TimelineModel } from '../../../../timelines/store/timeline/model'; import { EventDetails } from '../event_details'; import { TimelineDatePickerLock } from '../date_picker_lock'; import { HideShowContainer } from '../styles'; @@ -173,9 +173,6 @@ export const QueryTabContentComponent: React.FC = ({ kqlQueryExpression, ]); - const prevCombinedQueries = useRef<{ - filterQuery: string; - } | null>(null); const combinedQueries = useMemo( () => combineQueries({ @@ -211,12 +208,7 @@ export const QueryTabContentComponent: React.FC = ({ return [...columnFields, ...requiredFieldsForActions]; }, [columns]); - const prevTimelineQuerySortField = useRef< - Array<{ - field: string; - direction: Direction; - }> - >([]); + const timelineQuerySortField = useMemo( () => sort.map(({ columnId, sortDirection }) => ({ @@ -252,7 +244,7 @@ export const QueryTabContentComponent: React.FC = ({ }); const handleOnEventClosed = useCallback(() => { - onEventClosed({ timelineId }); + onEventClosed({ tabType: TimelineTabs.query, timelineId }); if (timelineId === TimelineId.active) { activeTimeline.toggleExpandedEvent({ @@ -266,17 +258,6 @@ export const QueryTabContentComponent: React.FC = ({ setIsTimelineLoading({ id: timelineId, isLoading: isQueryLoading || loadingSourcerer }); }, [loadingSourcerer, timelineId, isQueryLoading, setIsTimelineLoading]); - useEffect(() => { - if (!deepEqual(prevCombinedQueries.current, combinedQueries)) { - prevCombinedQueries.current = combinedQueries; - handleOnEventClosed(); - } - if (!deepEqual(prevTimelineQuerySortField.current, timelineQuerySortField)) { - prevTimelineQuerySortField.current = timelineQuerySortField; - handleOnEventClosed(); - } - }, [combinedQueries, handleOnEventClosed, timelineQuerySortField]); - return ( <> @@ -368,6 +349,7 @@ export const QueryTabContentComponent: React.FC = ({ @@ -416,7 +398,7 @@ const makeMapStateToProps = () => { dataProviders, eventType: eventType ?? 'raw', end: input.timerange.to, - expandedEvent, + expandedEvent: expandedEvent[TimelineTabs.query] ?? {}, filters: timelineFilter, timelineId, isLive: input.policy.kind === 'interval', @@ -425,7 +407,7 @@ const makeMapStateToProps = () => { kqlMode, kqlQueryExpression, showCallOutUnauthorizedMsg: getShowCallOutUnauthorizedMsg(state), - showEventDetails: !!expandedEvent.eventId, + showEventDetails: !!expandedEvent[TimelineTabs.query]?.eventId, show, sort, start: input.timerange.from, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx index c97571fbbd6f3..25312ac2747ae 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx @@ -8,16 +8,21 @@ import { EuiBadge, EuiLoadingContent, EuiTabs, EuiTab } from '@elastic/eui'; import React, { lazy, memo, Suspense, useCallback, useEffect, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; +import { TimelineTabs } from '../../../../../common/types/timeline'; -import { useShallowEqualSelector } from '../../../../common/hooks/use_selector'; +import { + useShallowEqualSelector, + useDeepEqualSelector, +} from '../../../../common/hooks/use_selector'; import { TimelineEventsCountBadge } from '../../../../common/hooks/use_timeline_events_count'; import { timelineActions } from '../../../store/timeline'; -import { TimelineTabs } from '../../../store/timeline/model'; import { getActiveTabSelector, + getNoteIdsSelector, getNotesSelector, getPinnedEventSelector, getShowTimelineSelector, + getEventIdToNoteIdsSelector, } from './selectors'; import * as i18n from './translations'; @@ -137,37 +142,55 @@ const TabsContentComponent: React.FC = ({ timelineId, graphEve const getActiveTab = useMemo(() => getActiveTabSelector(), []); const getShowTimeline = useMemo(() => getShowTimelineSelector(), []); const getNumberOfPinnedEvents = useMemo(() => getPinnedEventSelector(), []); - const getNumberOfNotes = useMemo(() => getNotesSelector(), []); + const getAppNotes = useMemo(() => getNotesSelector(), []); + const getTimelineNoteIds = useMemo(() => getNoteIdsSelector(), []); + const getTimelinePinnedEventNotes = useMemo(() => getEventIdToNoteIdsSelector(), []); + const activeTab = useShallowEqualSelector((state) => getActiveTab(state, timelineId)); const showTimeline = useShallowEqualSelector((state) => getShowTimeline(state, timelineId)); const numberOfPinnedEvents = useShallowEqualSelector((state) => getNumberOfPinnedEvents(state, timelineId) ); - const numberOfNotes = useShallowEqualSelector((state) => getNumberOfNotes(state)); + const globalTimelineNoteIds = useDeepEqualSelector((state) => + getTimelineNoteIds(state, timelineId) + ); + const eventIdToNoteIds = useDeepEqualSelector((state) => + getTimelinePinnedEventNotes(state, timelineId) + ); + const appNotes = useDeepEqualSelector((state) => getAppNotes(state)); + + const allTimelineNoteIds = useMemo(() => { + const eventNoteIds = Object.values(eventIdToNoteIds).reduce( + (acc, v) => [...acc, ...v], + [] + ); + return [...globalTimelineNoteIds, ...eventNoteIds]; + }, [globalTimelineNoteIds, eventIdToNoteIds]); + + const numberOfNotes = useMemo( + () => appNotes.filter((appNote) => allTimelineNoteIds.includes(appNote.id)).length, + [appNotes, allTimelineNoteIds] + ); const setQueryAsActiveTab = useCallback(() => { - dispatch(timelineActions.toggleExpandedEvent({ timelineId })); dispatch( timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.query }) ); }, [dispatch, timelineId]); const setGraphAsActiveTab = useCallback(() => { - dispatch(timelineActions.toggleExpandedEvent({ timelineId })); dispatch( timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.graph }) ); }, [dispatch, timelineId]); const setNotesAsActiveTab = useCallback(() => { - dispatch(timelineActions.toggleExpandedEvent({ timelineId })); dispatch( timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.notes }) ); }, [dispatch, timelineId]); const setPinnedAsActiveTab = useCallback(() => { - dispatch(timelineActions.toggleExpandedEvent({ timelineId })); dispatch( timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.pinned }) ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/selectors.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/selectors.ts index 332785161b09a..ff65c35588a8d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/selectors.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/selectors.ts @@ -5,8 +5,8 @@ */ import { createSelector } from 'reselect'; +import { TimelineTabs } from '../../../../../common/types/timeline'; import { selectNotesById } from '../../../../common/store/app/selectors'; -import { TimelineTabs } from '../../../store/timeline/model'; import { selectTimeline } from '../../../store/timeline/selectors'; export const getActiveTabSelector = () => @@ -18,5 +18,11 @@ export const getShowTimelineSelector = () => export const getPinnedEventSelector = () => createSelector(selectTimeline, (timeline) => Object.keys(timeline?.pinnedEventIds ?? {}).length); +export const getNoteIdsSelector = () => + createSelector(selectTimeline, (timeline) => timeline?.noteIds ?? []); + +export const getEventIdToNoteIdsSelector = () => + createSelector(selectTimeline, (timeline) => timeline?.eventIdToNoteIds ?? {}); + export const getNotesSelector = () => - createSelector(selectNotesById, (notesById) => Object.keys(notesById ?? {}).length); + createSelector(selectNotesById, (notesById) => Object.values(notesById)); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/active_timeline_context.ts b/x-pack/plugins/security_solution/public/timelines/containers/active_timeline_context.ts index 287fcd7f11e93..3d6d061157b29 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/active_timeline_context.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/active_timeline_context.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TimelineExpandedEvent } from '../../../common/types/timeline'; +import { TimelineExpandedEventType } from '../../../common/types/timeline'; import { TimelineEventsAllRequestOptions } from '../../../common/search_strategy/timeline'; import { TimelineArgs } from '.'; @@ -21,7 +21,7 @@ import { TimelineArgs } from '.'; class ActiveTimelineEvents { private _activePage: number = 0; - private _expandedEvent: TimelineExpandedEvent = {}; + private _expandedEvent: TimelineExpandedEventType = {}; private _pageName: string = ''; private _request: TimelineEventsAllRequestOptions | null = null; private _response: TimelineArgs | null = null; @@ -38,7 +38,7 @@ class ActiveTimelineEvents { return this._expandedEvent; } - toggleExpandedEvent(expandedEvent: TimelineExpandedEvent) { + toggleExpandedEvent(expandedEvent: TimelineExpandedEventType) { if (expandedEvent.eventId === this._expandedEvent.eventId) { this._expandedEvent = {}; } else { @@ -46,7 +46,7 @@ class ActiveTimelineEvents { } } - setExpandedEvent(expandedEvent: TimelineExpandedEvent) { + setExpandedEvent(expandedEvent: TimelineExpandedEventType) { this._expandedEvent = expandedEvent; } diff --git a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx index ebc86b3c5cf5e..556221f2d4bfd 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { noop } from 'lodash/fp'; +import { isEmpty, noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; import deepEqual from 'fast-deep-equal'; @@ -18,6 +18,7 @@ import { TimelineEventsDetailsStrategyResponse, } from '../../../../common/search_strategy'; import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/plugins/data/public'; +import { AbortError } from '../../../../../../../src/plugins/kibana_utils/common'; export interface EventsArgs { detailsData: TimelineEventsDetailsItem[] | null; } @@ -50,7 +51,7 @@ export const useTimelineEventsDetails = ({ const timelineDetailsSearch = useCallback( (request: TimelineEventsDetailsRequestOptions | null) => { - if (request == null || skip) { + if (request == null || skip || isEmpty(request.eventId)) { return; } @@ -84,11 +85,13 @@ export const useTimelineEventsDetails = ({ searchSubscription$.unsubscribe(); } }, - error: () => { + error: (msg) => { if (!didCancel) { setLoading(false); } - notifications.toasts.addDanger('Failed to run search'); + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger('Failed to run search'); + } }, }); }; 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 487dc171f5994..aefeda04dd962 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 @@ -15,13 +15,15 @@ import { } from '../../../timelines/components/timeline/data_providers/data_provider'; import { SerializedFilterQuery } from '../../../common/store/types'; -import { KqlMode, TimelineModel, ColumnHeaderOptions, TimelineTabs } from './model'; +import { KqlMode, TimelineModel, ColumnHeaderOptions } from './model'; import { TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; import { TimelineEventsType, - TimelineExpandedEvent, + TimelineExpandedEventType, TimelineTypeLiteral, RowRendererId, + TimelineExpandedEvent, + TimelineTabs, } from '../../../../common/types/timeline'; import { InsertTimeline } from './types'; @@ -36,8 +38,9 @@ export const addNoteToEvent = actionCreator<{ id: string; noteId: string; eventI ); export interface ToggleExpandedEvent { + event?: TimelineExpandedEventType; + tabType?: TimelineTabs; timelineId: string; - event?: TimelineExpandedEvent; } export const toggleExpandedEvent = actionCreator('TOGGLE_EXPANDED_EVENT'); 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 211bba3cc47d2..fd0d6bd3a9aaa 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 @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; +import { TimelineType, TimelineStatus, TimelineTabs } from '../../../../common/types/timeline'; import { Direction } from '../../../graphql/types'; import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers'; import { normalizeTimeRange } from '../../../common/components/url_state/normalize_time_range'; -import { SubsetTimelineModel, TimelineModel, TimelineTabs } from './model'; +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); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts index d890fbe6a1069..ec9ded610417f 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts @@ -5,10 +5,10 @@ */ import { Filter, esFilters } from '../../../../../../../src/plugins/data/public'; -import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; +import { TimelineType, TimelineStatus, TimelineTabs } from '../../../../common/types/timeline'; import { Direction } from '../../../graphql/types'; import { convertTimelineAsInput } from './epic'; -import { TimelineModel, TimelineTabs } from './model'; +import { TimelineModel } from './model'; describe('Epic Timeline', () => { describe('#convertTimelineAsInput ', () => { 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 3014ae8d19d32..513d61ea862fa 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 @@ -40,8 +40,7 @@ import { Direction } from '../../../graphql/types'; import { addTimelineInStorage } from '../../containers/local_storage'; import { isPageTimeline } from './epic_local_storage'; -import { TimelineId, TimelineStatus } from '../../../../common/types/timeline'; -import { TimelineTabs } from './model'; +import { TimelineId, TimelineStatus, TimelineTabs } from '../../../../common/types/timeline'; jest.mock('../../containers/local_storage'); 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 f3ff3fffa53b9..c385f21153780 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 @@ -17,11 +17,11 @@ import type { TimelineType, TimelineStatus, RowRendererId, + TimelineTabs, } from '../../../../common/types/timeline'; export const DEFAULT_PAGE_COUNT = 2; // Eui Pager will not render unless this is a minimum of 2 pages export type KqlMode = 'filter' | 'search'; - export type ColumnHeaderType = 'not-filtered' | 'text-filter'; /** Uniquely identifies a column */ @@ -43,13 +43,6 @@ export interface ColumnHeaderOptions { width: number; } -export enum TimelineTabs { - query = 'query', - graph = 'graph', - notes = 'notes', - pinned = 'pinned', -} - export interface TimelineModel { /** The selected tab to displayed in the timeline */ activeTab: TimelineTabs; 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 59d5800271b8a..4ae271ed7a491 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 @@ -5,7 +5,7 @@ */ import { cloneDeep } from 'lodash/fp'; -import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; +import { TimelineType, TimelineStatus, TimelineTabs } from '../../../../common/types/timeline'; import { IS_OPERATOR, @@ -40,7 +40,7 @@ import { updateTimelineTitle, upsertTimelineColumn, } from './helpers'; -import { ColumnHeaderOptions, TimelineModel, TimelineTabs } from './model'; +import { ColumnHeaderOptions, TimelineModel } from './model'; import { timelineDefaults } from './defaults'; import { TimelineById } from './types'; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts index 8ba4d54871266..2603c1c677956 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts @@ -103,7 +103,7 @@ import { } from './helpers'; import { TimelineState, EMPTY_TIMELINE_BY_ID } from './types'; -import { TimelineType } from '../../../../common/types/timeline'; +import { TimelineType, TimelineTabs } from '../../../../common/types/timeline'; export const initialTimelineState: TimelineState = { timelineById: EMPTY_TIMELINE_BY_ID, @@ -178,16 +178,22 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) ...state, timelineById: addTimelineNoteToEvent({ id, noteId, eventId, timelineById: state.timelineById }), })) - .case(toggleExpandedEvent, (state, { timelineId, event = {} }) => ({ - ...state, - timelineById: { - ...state.timelineById, - [timelineId]: { - ...state.timelineById[timelineId], - expandedEvent: event, + .case(toggleExpandedEvent, (state, { tabType, timelineId, event = {} }) => { + const expandedTabType = tabType ?? TimelineTabs.query; + return { + ...state, + timelineById: { + ...state.timelineById, + [timelineId]: { + ...state.timelineById[timelineId], + expandedEvent: { + ...state.timelineById[timelineId].expandedEvent, + [expandedTabType]: event, + }, + }, }, - }, - })) + }; + }) .case(addProvider, (state, { id, provider }) => ({ ...state, timelineById: addTimelineProvider({ id, provider, timelineById: state.timelineById }), 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 043066faa8010..f9899fb55bb6a 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 @@ -16,6 +16,7 @@ describe('create_signals', () => { size: 100, searchAfterSortId: undefined, timestampOverride: undefined, + excludeDocsWithTimestampOverride: false, }); expect(query).toEqual({ allowNoIndices: true, @@ -95,6 +96,7 @@ describe('create_signals', () => { size: 100, searchAfterSortId: '', timestampOverride: undefined, + excludeDocsWithTimestampOverride: false, }); expect(query).toEqual({ allowNoIndices: true, @@ -175,6 +177,7 @@ describe('create_signals', () => { size: 100, searchAfterSortId: fakeSortId, timestampOverride: undefined, + excludeDocsWithTimestampOverride: false, }); expect(query).toEqual({ allowNoIndices: true, @@ -256,6 +259,7 @@ describe('create_signals', () => { size: 100, searchAfterSortId: fakeSortIdNumber, timestampOverride: undefined, + excludeDocsWithTimestampOverride: false, }); expect(query).toEqual({ allowNoIndices: true, @@ -336,6 +340,7 @@ describe('create_signals', () => { size: 100, searchAfterSortId: undefined, timestampOverride: undefined, + excludeDocsWithTimestampOverride: false, }); expect(query).toEqual({ allowNoIndices: true, @@ -423,6 +428,7 @@ describe('create_signals', () => { size: 100, searchAfterSortId: undefined, timestampOverride: undefined, + excludeDocsWithTimestampOverride: false, }); 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 beca56770a9ca..31a424cdbcc1b 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 @@ -19,6 +19,7 @@ interface BuildEventsSearchQuery { sortOrder?: SortOrderOrUndefined; searchAfterSortId: string | number | undefined; timestampOverride: TimestampOverrideOrUndefined; + excludeDocsWithTimestampOverride: boolean; } export const buildEventsSearchQuery = ({ @@ -31,66 +32,65 @@ export const buildEventsSearchQuery = ({ searchAfterSortId, sortOrder, timestampOverride, + excludeDocsWithTimestampOverride, }: BuildEventsSearchQuery) => { - const timestamp = timestampOverride ?? '@timestamp'; - const docFields = - timestampOverride != null - ? [ - { - field: '@timestamp', - format: 'strict_date_optional_time', - }, - { - field: timestampOverride, - format: 'strict_date_optional_time', - }, - ] - : [ - { - field: '@timestamp', - format: 'strict_date_optional_time', - }, - ]; + const defaultTimeFields = ['@timestamp']; + const timestamps = + timestampOverride != null ? [timestampOverride, ...defaultTimeFields] : defaultTimeFields; + const docFields = timestamps.map((tstamp) => ({ + field: tstamp, + format: 'strict_date_optional_time', + })); + + const sortField = + timestampOverride != null && !excludeDocsWithTimestampOverride + ? timestampOverride + : '@timestamp'; - const filterWithTime = [ - filter, + const rangeFilter: unknown[] = [ { bool: { - filter: [ + should: [ { - bool: { - should: [ - { - range: { - [timestamp]: { - gte: from, - format: 'strict_date_optional_time', - }, - }, - }, - ], - minimum_should_match: 1, + range: { + [sortField]: { + gte: from, + format: 'strict_date_optional_time', + }, }, }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ { - bool: { - should: [ - { - range: { - [timestamp]: { - lte: to, - format: 'strict_date_optional_time', - }, - }, - }, - ], - minimum_should_match: 1, + range: { + [sortField]: { + lte: to, + format: 'strict_date_optional_time', + }, }, }, ], + minimum_should_match: 1, }, }, ]; + if (excludeDocsWithTimestampOverride) { + rangeFilter.push({ + bool: { + must_not: { + exists: { + field: timestampOverride, + }, + }, + }, + }); + } + const filterWithTime = [filter, { bool: { filter: rangeFilter } }]; const searchQuery = { allowNoIndices: true, @@ -112,7 +112,7 @@ export const buildEventsSearchQuery = ({ ...(aggregations ? { aggregations } : {}), sort: [ { - [timestamp]: { + [sortField]: { order: sortOrder ?? 'asc', }, }, 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 7141b61a23e6e..239edcd1f1845 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 @@ -85,5 +85,6 @@ export const findThresholdSignals = async ({ pageSize: 1, sortOrder: 'desc', buildRuleMessage, + excludeDocsWithTimestampOverride: false, }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 67246a830ce90..caac728f0a136 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -183,31 +183,6 @@ describe('searchAfterAndBulkCreate', () => { }, ], }) - .mockResolvedValueOnce(sampleDocSearchResultsNoSortIdNoHits()) - .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(9, 12))) - .mockResolvedValueOnce({ - took: 100, - errors: false, - items: [ - { - create: { - status: 201, - }, - }, - ], - }) - .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3))) - .mockResolvedValueOnce({ - took: 100, - errors: false, - items: [ - { - create: { - status: 201, - }, - }, - ], - }) .mockResolvedValueOnce(sampleDocSearchResultsNoSortIdNoHits()); const exceptionItem = getExceptionListItemSchemaMock(); @@ -250,8 +225,8 @@ describe('searchAfterAndBulkCreate', () => { buildRuleMessage, }); expect(success).toEqual(true); - expect(mockService.callCluster).toHaveBeenCalledTimes(12); - expect(createdSignalsCount).toEqual(5); + expect(mockService.callCluster).toHaveBeenCalledTimes(8); + expect(createdSignalsCount).toEqual(3); expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); @@ -461,7 +436,7 @@ describe('searchAfterAndBulkCreate', () => { // I don't like testing log statements since logs change but this is the best // way I can think of to ensure this section is getting hit with this test case. expect(((mockLogger.debug as unknown) as jest.Mock).mock.calls[8][0]).toContain( - 'sortIds was empty on searchResult' + 'ran out of sort ids to sort on name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' ); }); @@ -542,7 +517,7 @@ describe('searchAfterAndBulkCreate', () => { // I don't like testing log statements since logs change but this is the best // way I can think of to ensure this section is getting hit with this test case. expect(((mockLogger.debug as unknown) as jest.Mock).mock.calls[15][0]).toContain( - 'sortIds was empty on searchResult name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' + 'ran out of sort ids to sort on name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' ); }); 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 b79f758cd7503..fa47ef25a2db0 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 @@ -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. */ +/* eslint-disable complexity */ import { singleSearchAfter } from './single_search_after'; import { singleBulkCreate } from './single_bulk_create'; @@ -10,10 +11,12 @@ import { filterEventsAgainstList } from './filters/filter_events_against_list'; import { sendAlertTelemetryEvents } from './send_telemetry_events'; import { createSearchAfterReturnType, + createSearchResultReturnType, createSearchAfterReturnTypeFromResponse, createTotalHitsFromSearchResult, getSignalTimeTuples, mergeReturns, + mergeSearchResults, } from './utils'; import { SearchAfterAndBulkCreateParams, SearchAfterAndBulkCreateReturnType } from './types'; @@ -49,6 +52,9 @@ export const searchAfterAndBulkCreate = async ({ // sortId tells us where to start our next consecutive search_after query let sortId: string | undefined; + let hasSortId = true; // default to true so we execute the search on initial run + let backupSortId: string | undefined; + let hasBackupSortId = ruleParams.timestampOverride ? true : false; // signalsCreatedCount keeps track of how many signals we have created, // to ensure we don't exceed maxSignals @@ -78,10 +84,11 @@ export const searchAfterAndBulkCreate = async ({ signalsCreatedCount = 0; while (signalsCreatedCount < tuple.maxSignals) { try { + let mergedSearchResults = createSearchResultReturnType(); logger.debug(buildRuleMessage(`sortIds: ${sortId}`)); // perform search_after with optionally undefined sortId - const { searchResult, searchDuration, searchErrors } = await singleSearchAfter({ + const singleSearchAfterPromise = singleSearchAfter({ buildRuleMessage, searchAfterSortId: sortId, index: inputIndexPattern, @@ -92,23 +99,92 @@ export const searchAfterAndBulkCreate = async ({ filter, pageSize: tuple.maxSignals < pageSize ? Math.ceil(tuple.maxSignals) : pageSize, // maximum number of docs to receive per search result. timestampOverride: ruleParams.timestampOverride, + excludeDocsWithTimestampOverride: false, }); - toReturn = mergeReturns([ - toReturn, - createSearchAfterReturnTypeFromResponse({ - searchResult, + + // if there is a timestampOverride param we always want to do a secondary search against @timestamp + if (ruleParams.timestampOverride != null && hasBackupSortId) { + // only execute search if we have something to sort on or if it is the first search + const singleSearchAfterDefaultTimestamp = singleSearchAfter({ + buildRuleMessage, + searchAfterSortId: backupSortId, + index: inputIndexPattern, + from: tuple.from.toISOString(), + to: tuple.to.toISOString(), + services, + logger, + filter, + pageSize: tuple.maxSignals < pageSize ? Math.ceil(tuple.maxSignals) : pageSize, // maximum number of docs to receive per search result. timestampOverride: ruleParams.timestampOverride, - }), - createSearchAfterReturnType({ - searchAfterTimes: [searchDuration], - errors: searchErrors, - }), - ]); + excludeDocsWithTimestampOverride: true, + }); + const { + searchResult: searchResultB, + searchDuration: searchDurationB, + searchErrors: searchErrorsB, + } = await singleSearchAfterDefaultTimestamp; + + // call this function setSortIdOrExit() + const lastSortId = searchResultB?.hits?.hits[searchResultB.hits.hits.length - 1]?.sort; + if (lastSortId != null && lastSortId.length !== 0) { + backupSortId = lastSortId[0]; + hasBackupSortId = true; + } else { + // if no sort id on backup search and the initial search result was also empty + logger.debug(buildRuleMessage('backupSortIds was empty on searchResultB')); + hasBackupSortId = false; + } + + mergedSearchResults = mergeSearchResults([mergedSearchResults, searchResultB]); + + // merge the search result from the secondary search with the first + toReturn = mergeReturns([ + toReturn, + createSearchAfterReturnTypeFromResponse({ + searchResult: mergedSearchResults, + timestampOverride: undefined, + }), + createSearchAfterReturnType({ + searchAfterTimes: [searchDurationB], + errors: searchErrorsB, + }), + ]); + } + + if (hasSortId) { + // only execute search if we have something to sort on or if it is the first search + const { searchResult, searchDuration, searchErrors } = await singleSearchAfterPromise; + mergedSearchResults = mergeSearchResults([mergedSearchResults, searchResult]); + toReturn = mergeReturns([ + toReturn, + createSearchAfterReturnTypeFromResponse({ + searchResult: mergedSearchResults, + timestampOverride: ruleParams.timestampOverride, + }), + createSearchAfterReturnType({ + searchAfterTimes: [searchDuration], + errors: searchErrors, + }), + ]); + + // we are guaranteed to have searchResult hits at this point + // because we check before if the totalHits or + // searchResult.hits.hits.length is 0 + // call this function setSortIdOrExit() + const lastSortId = searchResult.hits.hits[searchResult.hits.hits.length - 1]?.sort; + if (lastSortId != null && lastSortId.length !== 0) { + sortId = lastSortId[0]; + hasSortId = true; + } else { + hasSortId = false; + } + } + // determine if there are any candidate signals to be processed - const totalHits = createTotalHitsFromSearchResult({ searchResult }); + const totalHits = createTotalHitsFromSearchResult({ searchResult: mergedSearchResults }); logger.debug(buildRuleMessage(`totalHits: ${totalHits}`)); logger.debug( - buildRuleMessage(`searchResult.hit.hits.length: ${searchResult.hits.hits.length}`) + buildRuleMessage(`searchResult.hit.hits.length: ${mergedSearchResults.hits.hits.length}`) ); // search results yielded zero hits so exit @@ -119,7 +195,7 @@ export const searchAfterAndBulkCreate = async ({ // e.g. totalHits was 156, index 50 of 100 results, do another search-after // this time with a new sortId, index 22 of the remaining 56, get another sortId // search with that sortId, total is still 156 but the hits.hits array is empty. - if (totalHits === 0 || searchResult.hits.hits.length === 0) { + if (totalHits === 0 || mergedSearchResults.hits.hits.length === 0) { logger.debug( buildRuleMessage( `${ @@ -137,7 +213,7 @@ export const searchAfterAndBulkCreate = async ({ listClient, exceptionsList, logger, - eventSearchResult: searchResult, + eventSearchResult: mergedSearchResults, buildRuleMessage, }); @@ -205,14 +281,8 @@ export const searchAfterAndBulkCreate = async ({ ); } - // we are guaranteed to have searchResult hits at this point - // because we check before if the totalHits or - // searchResult.hits.hits.length is 0 - const lastSortId = searchResult.hits.hits[searchResult.hits.hits.length - 1].sort; - if (lastSortId != null && lastSortId.length !== 0) { - sortId = lastSortId[0]; - } else { - logger.debug(buildRuleMessage('sortIds was empty on searchResult')); + if (!hasSortId && !hasBackupSortId) { + logger.debug(buildRuleMessage('ran out of sort ids to sort on')); break; } } catch (exc: unknown) { 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 9c2ea0945297e..f8983061d7a7a 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 @@ -16,6 +16,7 @@ import { getListsClient, getExceptions, sortExceptionItems, + checkPrivileges, } from './utils'; import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; import { RuleExecutorOptions, SearchAfterAndBulkCreateReturnType } from './types'; @@ -42,6 +43,7 @@ jest.mock('./utils', () => { getListsClient: jest.fn(), getExceptions: jest.fn(), sortExceptionItems: jest.fn(), + checkPrivileges: jest.fn(), }; }); jest.mock('../notifications/schedule_notification_actions'); @@ -105,6 +107,7 @@ describe('rules_notification_alert_type', () => { find: jest.fn(), goingToRun: jest.fn(), error: jest.fn(), + partialFailure: jest.fn(), }; (ruleStatusServiceFactory as jest.Mock).mockReturnValue(ruleStatusService); (getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(0)); @@ -124,6 +127,21 @@ describe('rules_notification_alert_type', () => { searchAfterTimes: [], createdSignalsCount: 10, }); + (checkPrivileges as jest.Mock).mockImplementation((_, indices) => { + return { + index: indices.reduce( + (acc: { index: { [x: string]: { read: boolean } } }, index: string) => { + return { + [index]: { + read: true, + }, + ...acc, + }; + }, + {} + ), + }; + }); alertServices.callCluster.mockResolvedValue({ hits: { total: { value: 10 }, @@ -170,6 +188,55 @@ describe('rules_notification_alert_type', () => { }); }); + it('should set a partial failure for when rules cannot read ALL provided indices', async () => { + (checkPrivileges as jest.Mock).mockResolvedValueOnce({ + username: 'elastic', + has_all_requested: false, + cluster: {}, + index: { + 'myfa*': { + read: true, + }, + 'anotherindex*': { + read: true, + }, + 'some*': { + read: false, + }, + }, + application: {}, + }); + payload.params.index = ['some*', 'myfa*', 'anotherindex*']; + await alert.executor(payload); + expect(ruleStatusService.partialFailure).toHaveBeenCalled(); + expect(ruleStatusService.partialFailure.mock.calls[0][0]).toContain( + 'Missing required read permissions on indexes: ["some*"]' + ); + }); + + it('should set a failure status for when rules cannot read ANY provided indices', async () => { + (checkPrivileges as jest.Mock).mockResolvedValueOnce({ + username: 'elastic', + has_all_requested: false, + cluster: {}, + index: { + 'myfa*': { + read: false, + }, + 'some*': { + read: false, + }, + }, + application: {}, + }); + payload.params.index = ['some*', 'myfa*']; + await alert.executor(payload); + expect(ruleStatusService.error).toHaveBeenCalled(); + expect(ruleStatusService.error.mock.calls[0][0]).toContain( + 'The rule does not have read privileges to any of the following indices: ["myfa*","some*"]' + ); + }); + 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({ 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 476b9aa56f572..8a219d926a96d 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 @@ -7,6 +7,7 @@ /* eslint-disable complexity */ import { Logger, KibanaRequest } from 'src/core/server'; +import { partition } from 'lodash'; import { SIGNALS_ID, @@ -41,6 +42,7 @@ import { createSearchAfterReturnType, mergeReturns, createSearchAfterReturnTypeFromResponse, + checkPrivileges, } from './utils'; import { signalParamsSchema } from './signal_params_schema'; import { siemRuleActionGroups } from './siem_rule_action_groups'; @@ -171,8 +173,48 @@ export const signalRulesAlertType = ({ logger.debug(buildRuleMessage('[+] Starting Signal Rule execution')); logger.debug(buildRuleMessage(`interval: ${interval}`)); + let wroteStatus = false; await ruleStatusService.goingToRun(); + // check if rule has permissions to access given index pattern + // move this collection of lines into a function in utils + // so that we can use it in create rules route, bulk, etc. + try { + const inputIndex = await getInputIndex(services, version, index); + const privileges = await checkPrivileges(services, inputIndex); + + const indexNames = Object.keys(privileges.index); + const [indexesWithReadPrivileges, indexesWithNoReadPrivileges] = partition( + indexNames, + (indexName) => privileges.index[indexName].read + ); + + if (indexesWithReadPrivileges.length > 0 && indexesWithNoReadPrivileges.length > 0) { + // some indices have read privileges others do not. + // set a partial failure status + const errorString = `Missing required read permissions on indexes: ${JSON.stringify( + indexesWithNoReadPrivileges + )}`; + logger.debug(buildRuleMessage(errorString)); + await ruleStatusService.partialFailure(errorString); + wroteStatus = true; + } else if ( + indexesWithReadPrivileges.length === 0 && + indexesWithNoReadPrivileges.length === indexNames.length + ) { + // none of the indices had read privileges so set the status to failed + // since we can't search on any indices we do not have read privileges on + const errorString = `The rule does not have read privileges to any of the following indices: ${JSON.stringify( + indexesWithNoReadPrivileges + )}`; + logger.debug(buildRuleMessage(errorString)); + await ruleStatusService.error(errorString); + wroteStatus = true; + } + } catch (exc) { + logger.error(buildRuleMessage(`Check privileges failed to execute ${exc}`)); + } + const gap = getGapBetweenRuns({ previousStartedAt, interval, from, to }); if (gap != null && gap.asMilliseconds() > 0) { const fromUnit = from[from.length - 1]; @@ -600,7 +642,7 @@ export const signalRulesAlertType = ({ `[+] Finished indexing ${result.createdSignalsCount} signals into ${outputIndex}` ) ); - if (!hasError) { + if (!hasError && !wroteStatus) { await ruleStatusService.success('succeeded', { bulkCreateTimeDurations: result.bulkCreateTimes, searchAfterTimeDurations: result.searchAfterTimes, 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 c4869f024a977..12d91dcde2ff7 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 @@ -40,6 +40,7 @@ describe('singleSearchAfter', () => { filter: undefined, timestampOverride: undefined, buildRuleMessage, + excludeDocsWithTimestampOverride: false, }); expect(searchResult).toEqual(sampleDocSearchResultsNoSortId()); }); @@ -56,6 +57,7 @@ describe('singleSearchAfter', () => { filter: undefined, timestampOverride: undefined, buildRuleMessage, + excludeDocsWithTimestampOverride: false, }); expect(searchErrors).toEqual([]); }); @@ -104,9 +106,10 @@ describe('singleSearchAfter', () => { filter: undefined, timestampOverride: undefined, buildRuleMessage, + excludeDocsWithTimestampOverride: false, }); expect(searchErrors).toEqual([ - 'reason: "some reason" type: "some type" caused by reason: "some reason" caused by type: "some type"', + 'index: "index-123" reason: "some reason" type: "some type" caused by reason: "some reason" caused by type: "some type"', ]); }); test('if singleSearchAfter works with a given sort id', async () => { @@ -123,6 +126,7 @@ describe('singleSearchAfter', () => { filter: undefined, timestampOverride: undefined, buildRuleMessage, + excludeDocsWithTimestampOverride: false, }); expect(searchResult).toEqual(sampleDocSearchResultsWithSortId()); }); @@ -143,6 +147,7 @@ describe('singleSearchAfter', () => { filter: undefined, timestampOverride: undefined, buildRuleMessage, + excludeDocsWithTimestampOverride: false, }) ).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 23ef9fcea8e53..79e1f9896d63f 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 @@ -29,6 +29,7 @@ interface SingleSearchAfterParams { filter: unknown; timestampOverride: TimestampOverrideOrUndefined; buildRuleMessage: BuildRuleMessage; + excludeDocsWithTimestampOverride: boolean; } // utilize search_after for paging results into bulk. @@ -45,6 +46,7 @@ export const singleSearchAfter = async ({ sortOrder, timestampOverride, buildRuleMessage, + excludeDocsWithTimestampOverride, }: SingleSearchAfterParams): Promise<{ searchResult: SignalSearchResponse; searchDuration: string; @@ -61,6 +63,7 @@ export const singleSearchAfter = async ({ sortOrder, searchAfterSortId, timestampOverride, + excludeDocsWithTimestampOverride, }); const start = performance.now(); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_find_previous_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_find_previous_signals.ts index 960693bc703d6..6e7f63deb06f7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_find_previous_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_find_previous_signals.ts @@ -83,5 +83,6 @@ export const findPreviousThresholdSignals = async ({ filter, pageSize: 0, buildRuleMessage, + excludeDocsWithTimestampOverride: false, }); }; 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 073e30bbc6e26..b410fb7c35be0 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 @@ -879,7 +879,7 @@ describe('utils', () => { ]; const createdErrors = createErrorsFromShard({ errors }); expect(createdErrors).toEqual([ - 'reason: "some reason" type: "some type" caused by reason: "some reason" caused by type: "some type"', + 'index: "index-123" reason: "some reason" type: "some type" caused by reason: "some reason" caused by type: "some type"', ]); }); @@ -918,8 +918,8 @@ describe('utils', () => { ]; const createdErrors = createErrorsFromShard({ errors }); expect(createdErrors).toEqual([ - 'reason: "some reason" type: "some type" caused by reason: "some reason" caused by type: "some type"', - 'reason: "some reason 2" type: "some type 2" caused by reason: "some reason 2" caused by type: "some type 2"', + 'index: "index-123" reason: "some reason" type: "some type" caused by reason: "some reason" caused by type: "some type"', + 'index: "index-345" reason: "some reason 2" type: "some type 2" caused by reason: "some reason 2" caused by type: "some type 2"', ]); }); @@ -933,7 +933,7 @@ describe('utils', () => { }, ]; const createdErrors = createErrorsFromShard({ errors }); - expect(createdErrors).toEqual(['']); + expect(createdErrors).toEqual(['index: "index-123"']); }); test('You can have a single value for the shard errors and get expected output without extra spaces anywhere', () => { @@ -948,7 +948,9 @@ describe('utils', () => { }, ]; const createdErrors = createErrorsFromShard({ errors }); - expect(createdErrors).toEqual(['reason: "some reason something went wrong"']); + expect(createdErrors).toEqual([ + 'index: "index-123" reason: "some reason something went wrong"', + ]); }); test('You can have two values for the shard errors and get expected output with one space exactly between the two values', () => { @@ -965,7 +967,7 @@ describe('utils', () => { ]; const createdErrors = createErrorsFromShard({ errors }); expect(createdErrors).toEqual([ - 'reason: "some reason something went wrong" caused by type: "some type"', + 'index: "index-123" reason: "some reason something went wrong" caused by type: "some type"', ]); }); }); 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 18f6e8d127b1b..ab14643f30e41 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 @@ -52,6 +52,20 @@ export const shorthandMap = { }, }; +export const checkPrivileges = async (services: AlertServices, indices: string[]) => + services.callCluster('transport.request', { + path: '/_security/user/_has_privileges', + method: 'POST', + body: { + index: [ + { + names: indices ?? [], + privileges: ['read'], + }, + ], + }, + }); + export const getGapMaxCatchupRatio = ({ logger, previousStartedAt, @@ -516,6 +530,7 @@ export const getSignalTimeTuples = ({ export const createErrorsFromShard = ({ errors }: { errors: ShardError[] }): string[] => { return errors.map((error) => { const { + index, reason: { reason, type, @@ -527,6 +542,7 @@ export const createErrorsFromShard = ({ errors }: { errors: ShardError[] }): str } = error; return [ + ...(index != null ? [`index: "${index}"`] : []), ...(reason != null ? [`reason: "${reason}"`] : []), ...(type != null ? [`type: "${type}"`] : []), ...(causedByReason != null ? [`caused by reason: "${causedByReason}"`] : []), @@ -615,6 +631,25 @@ export const createSearchAfterReturnType = ({ }; }; +export const createSearchResultReturnType = (): SignalSearchResponse => { + return { + took: 0, + timed_out: false, + _shards: { + total: 0, + successful: 0, + failed: 0, + skipped: 0, + failures: [], + }, + hits: { + total: 0, + max_score: 0, + hits: [], + }, + }; +}; + export const mergeReturns = ( searchAfters: SearchAfterAndBulkCreateReturnType[] ): SearchAfterAndBulkCreateReturnType => { @@ -651,6 +686,52 @@ export const mergeReturns = ( }); }; +export const mergeSearchResults = (searchResults: SignalSearchResponse[]) => { + return searchResults.reduce((prev, next) => { + const { + took: existingTook, + timed_out: existingTimedOut, + // _scroll_id: existingScrollId, + _shards: existingShards, + // aggregations: existingAggregations, + hits: existingHits, + } = prev; + + const { + took: newTook, + timed_out: newTimedOut, + _scroll_id: newScrollId, + _shards: newShards, + aggregations: newAggregations, + hits: newHits, + } = next; + + return { + took: Math.max(newTook, existingTook), + timed_out: newTimedOut && existingTimedOut, + _scroll_id: newScrollId, + _shards: { + total: newShards.total + existingShards.total, + successful: newShards.successful + existingShards.successful, + failed: newShards.failed + existingShards.failed, + skipped: newShards.skipped + existingShards.skipped, + failures: [ + ...(existingShards.failures != null ? existingShards.failures : []), + ...(newShards.failures != null ? newShards.failures : []), + ], + }, + aggregations: newAggregations, + hits: { + total: + createTotalHitsFromSearchResult({ searchResult: prev }) + + createTotalHitsFromSearchResult({ searchResult: next }), + max_score: Math.max(newHits.max_score, existingHits.max_score), + hits: [...existingHits.hits, ...newHits.hits], + }, + }; + }); +}; + export const createTotalHitsFromSearchResult = ({ searchResult, }: { diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/task.ts b/x-pack/plugins/security_solution/server/lib/telemetry/task.ts index 28b8524f64516..a723cb9a3e637 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/task.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/task.ts @@ -36,6 +36,8 @@ export class TelemetryDiagTask { title: 'Security Solution Telemetry Diagnostics task', timeout: TelemetryDiagTaskConstants.TIMEOUT, createTaskRunner: ({ taskInstance }: { taskInstance: ConcreteTaskInstance }) => { + const { state } = taskInstance; + return { run: async () => { const executeTo = moment().utc().toISOString(); @@ -43,11 +45,13 @@ export class TelemetryDiagTask { executeTo, taskInstance.state?.lastExecutionTimestamp ); - await this.runTask(taskInstance.id, executeFrom, executeTo); + const hits = await this.runTask(taskInstance.id, executeFrom, executeTo); return { state: { lastExecutionTimestamp: executeTo, + lastDiagAlertCount: hits, + runs: (state.runs || 0) + 1, }, }; }, @@ -81,7 +85,7 @@ export class TelemetryDiagTask { schedule: { interval: TelemetryDiagTaskConstants.INTERVAL, }, - state: {}, + state: { runs: 0 }, params: { version: TelemetryDiagTaskConstants.VERSION }, }); } catch (e) { @@ -97,13 +101,13 @@ export class TelemetryDiagTask { this.logger.debug(`Running task ${taskId}`); if (taskId !== this.getTaskId()) { this.logger.debug(`Outdated task running: ${taskId}`); - return; + return 0; } const isOptedIn = await this.sender.isTelemetryOptedIn(); if (!isOptedIn) { this.logger.debug(`Telemetry is not opted-in.`); - return; + return 0; } const response = await this.sender.fetchDiagnosticAlerts(searchFrom, searchTo); @@ -111,11 +115,12 @@ export class TelemetryDiagTask { const hits = response.hits?.hits || []; if (!Array.isArray(hits) || !hits.length) { this.logger.debug('no diagnostic alerts retrieved'); - return; + return 0; } + this.logger.debug(`Received ${hits.length} diagnostic alerts`); const diagAlerts: TelemetryEvent[] = hits.map((h) => h._source); - this.logger.debug(`Received ${diagAlerts.length} diagnostic alerts`); this.sender.queueTelemetryEvents(diagAlerts); + return diagAlerts.length; }; } diff --git a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts index ea8770b7843cf..747e37e7db32b 100644 --- a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts +++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts @@ -12,7 +12,10 @@ import { ILicense, LicensingPluginSetup } from '../../../licensing/server'; import { UsageStats } from '../usage_stats'; import { usageStatsClientMock } from '../usage_stats/usage_stats_client.mock'; import { usageStatsServiceMock } from '../usage_stats/usage_stats_service.mock'; -import { pluginInitializerContextConfigMock } from 'src/core/server/mocks'; +import { + elasticsearchServiceMock, + pluginInitializerContextConfigMock, +} from 'src/core/server/mocks'; import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; interface SetupOpts { @@ -74,31 +77,39 @@ function setup({ }; } -const defaultCallClusterMock = jest.fn().mockResolvedValue({ - hits: { - total: { - value: 2, +const defaultEsClientSearchMock = jest.fn().mockResolvedValue({ + body: { + hits: { + total: { + value: 2, + }, }, - }, - aggregations: { - disabledFeatures: { - buckets: [ - { - key: 'feature1', - doc_count: 1, - }, - ], + aggregations: { + disabledFeatures: { + buckets: [ + { + key: 'feature1', + doc_count: 1, + }, + ], + }, }, }, }); -const getMockFetchContext = (mockedCallCluster: jest.Mock) => { +const getMockFetchContext = (mockedEsClient: any) => { return { ...createCollectorFetchContextMock(), - callCluster: mockedCallCluster, + esClient: mockedEsClient, }; }; +const getMockedEsClient = (esClientMock: jest.Mock) => { + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + esClient.search = esClientMock; + return esClient; +}; + describe('error handling', () => { it('handles a 404 when searching for space usage', async () => { const { features, licensing, usageCollection, usageStatsService } = setup({ @@ -110,8 +121,10 @@ describe('error handling', () => { licensing, usageStatsServicePromise: Promise.resolve(usageStatsService), }); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + esClient.search.mockRejectedValue({ status: 404 }); - await collector.fetch(getMockFetchContext(jest.fn().mockRejectedValue({ status: 404 }))); + await collector.fetch(getMockFetchContext(esClient)); }); it('throws error for a non-404', async () => { @@ -124,13 +137,13 @@ describe('error handling', () => { licensing, usageStatsServicePromise: Promise.resolve(usageStatsService), }); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const statusCodes = [401, 402, 403, 500]; for (const statusCode of statusCodes) { const error = { status: statusCode }; - await expect( - collector.fetch(getMockFetchContext(jest.fn().mockRejectedValue(error))) - ).rejects.toBe(error); + esClient.search.mockRejectedValue(error); + await expect(collector.fetch(getMockFetchContext(esClient))).rejects.toBe(error); } }); }); @@ -148,9 +161,10 @@ describe('with a basic license', () => { licensing, usageStatsServicePromise: Promise.resolve(usageStatsService), }); - usageData = await collector.fetch(getMockFetchContext(defaultCallClusterMock)); + const esClient = getMockedEsClient(defaultEsClientSearchMock); + usageData = await collector.fetch(getMockFetchContext(esClient)); - expect(defaultCallClusterMock).toHaveBeenCalledWith('search', { + expect(defaultEsClientSearchMock).toHaveBeenCalledWith({ body: { aggs: { disabledFeatures: { @@ -206,7 +220,9 @@ describe('with no license', () => { licensing, usageStatsServicePromise: Promise.resolve(usageStatsService), }); - usageData = await collector.fetch(getMockFetchContext(defaultCallClusterMock)); + const esClient = getMockedEsClient(defaultEsClientSearchMock); + + usageData = await collector.fetch(getMockFetchContext(esClient)); }); test('sets enabled to false', () => { @@ -245,7 +261,9 @@ describe('with platinum license', () => { licensing, usageStatsServicePromise: Promise.resolve(usageStatsService), }); - usageData = await collector.fetch(getMockFetchContext(defaultCallClusterMock)); + const esClient = getMockedEsClient(defaultEsClientSearchMock); + + usageData = await collector.fetch(getMockFetchContext(esClient)); }); test('sets enabled to true', () => { diff --git a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts index 44388453d0707..269490bddd8dc 100644 --- a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts +++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts @@ -4,19 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyCallAPIOptions } from 'src/core/server'; +import { ElasticsearchClient } from 'src/core/server'; import { take } from 'rxjs/operators'; import { CollectorFetchContext, UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { Observable } from 'rxjs'; import { PluginsSetup } from '../plugin'; import { UsageStats, UsageStatsServiceSetup } from '../usage_stats'; -type CallCluster = ( - endpoint: string, - clientParams: Record, - options?: LegacyCallAPIOptions -) => Promise; - interface SpacesAggregationResponse { hits: { total: { value: number }; @@ -37,7 +31,7 @@ interface SpacesAggregationResponse { * @return {UsageData} */ async function getSpacesUsage( - callCluster: CallCluster, + esClient: ElasticsearchClient, kibanaIndex: string, features: PluginsSetup['features'], spacesAvailable: boolean @@ -50,7 +44,7 @@ async function getSpacesUsage( let resp: SpacesAggregationResponse | undefined; try { - resp = await callCluster('search', { + ({ body: resp } = await esClient.search({ index: kibanaIndex, body: { track_total_hits: true, @@ -72,7 +66,7 @@ async function getSpacesUsage( }, size: 0, }, - }); + })); } catch (err) { if (err.status === 404) { return null; @@ -208,14 +202,14 @@ export function getSpacesUsageCollector( 'apiCalls.resolveCopySavedObjectsErrors.createNewCopiesEnabled.yes': { type: 'long' }, 'apiCalls.resolveCopySavedObjectsErrors.createNewCopiesEnabled.no': { type: 'long' }, }, - fetch: async ({ callCluster }: CollectorFetchContext) => { + fetch: async ({ esClient }: CollectorFetchContext) => { const { licensing, kibanaIndexConfig$, features, usageStatsServicePromise } = deps; const license = await licensing.license$.pipe(take(1)).toPromise(); const available = license.isAvailable; // some form of spaces is available for all valid licenses const kibanaIndex = (await kibanaIndexConfig$.pipe(take(1)).toPromise()).kibana.index; - const usageData = await getSpacesUsage(callCluster, kibanaIndex, features, available); + const usageData = await getSpacesUsage(esClient, kibanaIndex, features, available); const usageStats = await getUsageStats(usageStatsServicePromise, available); return { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b61532ec88c3c..fda6b81c4af03 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -248,6 +248,7 @@ "charts.colormaps.redsText": "赤", "charts.colormaps.yellowToRedText": "黄色から赤", "charts.colorPicker.setColor.screenReaderDescription": "値 {legendDataLabel} の色を設定", + "charts.countText": "カウント", "console.autocomplete.addMethodMetaText": "メソド", "console.consoleDisplayName": "コンソール", "console.consoleMenu.copyAsCurlMessage": "リクエストが URL としてコピーされました", @@ -1349,6 +1350,8 @@ "data.search.functions.kibana_context.savedSearchId.help": "クエリとフィルターに使用する保存検索ID を指定します。", "data.search.functions.kibana_context.timeRange.help": "Kibana 時間範囲フィルターを指定します", "data.search.functions.kibana.help": "Kibana グローバルコンテキストを取得します", + "data.triggers.applyFilterDescription": "Kibanaフィルターが適用されるとき。単一の値または範囲フィルターにすることができます。", + "data.triggers.applyFilterTitle": "フィルターを適用", "devTools.badge.readOnly.text": "読み込み専用", "devTools.badge.readOnly.tooltip": "を保存できませんでした", "devTools.devToolsTitle": "開発ツール", @@ -1574,6 +1577,10 @@ "embeddableApi.samples.contactCard.displayName": "連絡先カード", "embeddableApi.samples.filterableContainer.displayName": "フィルター可能なダッシュボード", "embeddableApi.samples.filterableEmbeddable.displayName": "フィルター可能", + "embeddableApi.selectRangeTrigger.description": "ビジュアライゼーションでの値の範囲", + "embeddableApi.selectRangeTrigger.title": "範囲選択", + "embeddableApi.valueClickTrigger.description": "ビジュアライゼーションでデータポイントをクリック", + "embeddableApi.valueClickTrigger.title": "シングルクリック", "esUi.cronEditor.cronDaily.fieldHour.textAtLabel": "に", "esUi.cronEditor.cronDaily.fieldTimeLabel": "時間", "esUi.cronEditor.cronDaily.hourSelectLabel": "時間", @@ -3482,12 +3489,6 @@ "uiActions.actionPanel.more": "詳細", "uiActions.actionPanel.title": "オプション", "uiActions.errors.incompatibleAction": "操作に互換性がありません", - "data.triggers.applyFilterDescription": "Kibanaフィルターが適用されるとき。単一の値または範囲フィルターにすることができます。", - "data.triggers.applyFilterTitle": "フィルターを適用", - "embeddableApi.selectRangeTrigger.description": "ビジュアライゼーションでの値の範囲", - "embeddableApi.selectRangeTrigger.title": "範囲選択", - "embeddableApi.valueClickTrigger.description": "ビジュアライゼーションでデータポイントをクリック", - "embeddableApi.valueClickTrigger.title": "シングルクリック", "usageCollection.stats.notReadyMessage": "まだ統計が準備できていません。しばらくたってから再試行してください。", "visDefaultEditor.advancedToggle.advancedLinkLabel": "高度な設定", "visDefaultEditor.agg.toggleEditorButtonAriaLabel": "{schema} エディターを切り替える", @@ -4305,27 +4306,6 @@ "visTypeVislib.advancedSettings.visualization.heatmap.maxBucketsText": "1つのデータソースが返せるバケットの最大数です。値が大きいとブラウザのレンダリング速度が下がる可能性があります。", "visTypeVislib.advancedSettings.visualization.heatmap.maxBucketsTitle": "ヒートマップの最大バケット数", "visTypeVislib.aggResponse.allDocsTitle": "すべてのドキュメント", - "visTypeXy.area.areaTitle": "エリア", - "charts.countText": "カウント", - "visTypeXy.area.groupTitle": "系列を分割", - "visTypeXy.area.metricsTitle": "Y 軸", - "visTypeXy.area.radiusTitle": "点のサイズ", - "visTypeXy.area.segmentTitle": "X 軸", - "visTypeXy.area.splitTitle": "チャートを分割", - "visTypeXy.area.tabs.metricsAxesTitle": "メトリックと軸", - "visTypeXy.area.tabs.panelSettingsTitle": "パネル設定", - "visTypeXy.axisModes.normalText": "標準", - "visTypeXy.axisModes.percentageText": "割合 (%)", - "visTypeXy.axisModes.silhouetteText": "シルエット", - "visTypeXy.axisModes.wiggleText": "振動", - "visTypeXy.categoryAxis.rotate.angledText": "傾斜", - "visTypeXy.categoryAxis.rotate.horizontalText": "横", - "visTypeXy.categoryAxis.rotate.verticalText": "縦", - "visTypeXy.chartModes.normalText": "標準", - "visTypeXy.chartModes.stackedText": "スタック", - "visTypeXy.chartTypes.areaText": "エリア", - "visTypeXy.chartTypes.barText": "バー", - "visTypeXy.chartTypes.lineText": "折れ線", "visTypeVislib.controls.gaugeOptions.alignmentLabel": "アラインメント", "visTypeVislib.controls.gaugeOptions.autoExtendRangeLabel": "範囲を自動拡張", "visTypeVislib.controls.gaugeOptions.displayWarningsLabel": "警告を表示", @@ -4351,6 +4331,68 @@ "visTypeVislib.controls.heatmapOptions.scaleToDataBoundsLabel": "データバウンドに合わせる", "visTypeVislib.controls.heatmapOptions.showLabelsTitle": "ラベルを表示", "visTypeVislib.controls.heatmapOptions.useCustomRangesLabel": "カスタム範囲を使用", + "visTypeVislib.editors.heatmap.basicSettingsTitle": "基本設定", + "visTypeVislib.editors.heatmap.heatmapSettingsTitle": "ヒートマップ設定", + "visTypeVislib.editors.heatmap.highlightLabel": "ハイライト範囲", + "visTypeVislib.editors.heatmap.highlightLabelTooltip": "チャートのカーソルを当てた部分と凡例の対応するラベルをハイライトします。", + "visTypeVislib.editors.pie.donutLabel": "ドーナッツ", + "visTypeVislib.editors.pie.labelsSettingsTitle": "ラベル設定", + "visTypeVislib.editors.pie.pieSettingsTitle": "パイ設定", + "visTypeVislib.editors.pie.showLabelsLabel": "ラベルを表示", + "visTypeVislib.editors.pie.showTopLevelOnlyLabel": "トップレベルのみ表示", + "visTypeVislib.editors.pie.showValuesLabel": "値を表示", + "visTypeVislib.functions.pie.help": "パイビジュアライゼーション", + "visTypeVislib.functions.vislib.help": "Vislib ビジュアライゼーション", + "visTypeVislib.gauge.alignmentAutomaticTitle": "自動", + "visTypeVislib.gauge.alignmentHorizontalTitle": "横", + "visTypeVislib.gauge.alignmentVerticalTitle": "縦", + "visTypeVislib.gauge.gaugeTitle": "ゲージ", + "visTypeVislib.gauge.gaugeTypes.arcText": "弧形", + "visTypeVislib.gauge.gaugeTypes.circleText": "円", + "visTypeVislib.gauge.groupTitle": "グループを分割", + "visTypeVislib.gauge.metricTitle": "メトリック", + "visTypeVislib.goal.goalTitle": "ゴール", + "visTypeVislib.goal.groupTitle": "グループを分割", + "visTypeVislib.goal.metricTitle": "メトリック", + "visTypeVislib.heatmap.groupTitle": "Y 軸", + "visTypeVislib.heatmap.metricTitle": "値", + "visTypeVislib.heatmap.segmentTitle": "X 軸", + "visTypeVislib.heatmap.splitTitle": "チャートを分割", + "visTypeVislib.pie.metricTitle": "サイズのスライス", + "visTypeVislib.pie.pieTitle": "パイ", + "visTypeVislib.pie.segmentTitle": "スライスの分割", + "visTypeVislib.pie.splitTitle": "チャートを分割", + "visTypeVislib.vislib.errors.noResultsFoundTitle": "結果が見つかりませんでした", + "visTypeVislib.vislib.heatmap.maxBucketsText": "定義された数列が多すぎます ({nr})。構成されている最大値は {max} です。", + "visTypeVislib.vislib.legend.filterForValueButtonAriaLabel": "値 {legendDataLabel} でフィルタリング", + "visTypeVislib.vislib.legend.filterOptionsLegend": "{legendDataLabel}、フィルターオプション", + "visTypeVislib.vislib.legend.filterOutValueButtonAriaLabel": "値 {legendDataLabel} を除外", + "visTypeVislib.vislib.legend.loadingLabel": "読み込み中…", + "visTypeVislib.vislib.legend.toggleLegendButtonAriaLabel": "凡例を切り替える", + "visTypeVislib.vislib.legend.toggleLegendButtonTitle": "凡例を切り替える", + "visTypeVislib.vislib.legend.toggleOptionsButtonAriaLabel": "{legendDataLabel}、トグルオプション", + "visTypeVislib.vislib.tooltip.fieldLabel": "フィールド", + "visTypeVislib.vislib.tooltip.valueLabel": "値", + "visTypeXy.area.areaTitle": "エリア", + "visTypeXy.area.groupTitle": "系列を分割", + "visTypeXy.area.metricsTitle": "Y 軸", + "visTypeXy.area.radiusTitle": "点のサイズ", + "visTypeXy.area.segmentTitle": "X 軸", + "visTypeXy.area.splitTitle": "チャートを分割", + "visTypeXy.area.tabs.metricsAxesTitle": "メトリックと軸", + "visTypeXy.area.tabs.panelSettingsTitle": "パネル設定", + "visTypeXy.axisModes.normalText": "標準", + "visTypeXy.axisModes.percentageText": "割合 (%)", + "visTypeXy.axisModes.silhouetteText": "シルエット", + "visTypeXy.axisModes.wiggleText": "振動", + "visTypeXy.categoryAxis.rotate.angledText": "傾斜", + "visTypeXy.categoryAxis.rotate.horizontalText": "横", + "visTypeXy.categoryAxis.rotate.verticalText": "縦", + "visTypeXy.chartModes.normalText": "標準", + "visTypeXy.chartModes.stackedText": "スタック", + "visTypeXy.chartTypes.areaText": "エリア", + "visTypeXy.chartTypes.barText": "バー", + "visTypeXy.chartTypes.lineText": "折れ線", "visTypeXy.controls.pointSeries.categoryAxis.alignLabel": "配置", "visTypeXy.controls.pointSeries.categoryAxis.filterLabelsLabel": "フィルターラベル", "visTypeXy.controls.pointSeries.categoryAxis.labelsTitle": "ラベル", @@ -4393,16 +4435,6 @@ "visTypeXy.controls.pointSeries.valueAxes.toggleOptionsAriaLabel": "{axisName} オプションを切り替える", "visTypeXy.controls.pointSeries.valueAxes.yAxisTitle": "Y 軸", "visTypeXy.controls.truncateLabel": "切り捨て", - "visTypeVislib.editors.heatmap.basicSettingsTitle": "基本設定", - "visTypeVislib.editors.heatmap.heatmapSettingsTitle": "ヒートマップ設定", - "visTypeVislib.editors.heatmap.highlightLabel": "ハイライト範囲", - "visTypeVislib.editors.heatmap.highlightLabelTooltip": "チャートのカーソルを当てた部分と凡例の対応するラベルをハイライトします。", - "visTypeVislib.editors.pie.donutLabel": "ドーナッツ", - "visTypeVislib.editors.pie.labelsSettingsTitle": "ラベル設定", - "visTypeVislib.editors.pie.pieSettingsTitle": "パイ設定", - "visTypeVislib.editors.pie.showLabelsLabel": "ラベルを表示", - "visTypeVislib.editors.pie.showTopLevelOnlyLabel": "トップレベルのみ表示", - "visTypeVislib.editors.pie.showValuesLabel": "値を表示", "visTypeXy.editors.pointSeries.currentTimeMarkerLabel": "現在時刻マーカー", "visTypeXy.editors.pointSeries.orderBucketsBySumLabel": "バケットを合計で並べ替え", "visTypeXy.editors.pointSeries.settingsTitle": "設定", @@ -4413,23 +4445,6 @@ "visTypeXy.editors.pointSeries.thresholdLine.valueLabel": "しきい値", "visTypeXy.editors.pointSeries.thresholdLine.widthLabel": "線の幅", "visTypeXy.editors.pointSeries.thresholdLineSettingsTitle": "しきい線", - "visTypeVislib.functions.pie.help": "パイビジュアライゼーション", - "visTypeVislib.functions.vislib.help": "Vislib ビジュアライゼーション", - "visTypeVislib.gauge.alignmentAutomaticTitle": "自動", - "visTypeVislib.gauge.alignmentHorizontalTitle": "横", - "visTypeVislib.gauge.alignmentVerticalTitle": "縦", - "visTypeVislib.gauge.gaugeTitle": "ゲージ", - "visTypeVislib.gauge.gaugeTypes.arcText": "弧形", - "visTypeVislib.gauge.gaugeTypes.circleText": "円", - "visTypeVislib.gauge.groupTitle": "グループを分割", - "visTypeVislib.gauge.metricTitle": "メトリック", - "visTypeVislib.goal.goalTitle": "ゴール", - "visTypeVislib.goal.groupTitle": "グループを分割", - "visTypeVislib.goal.metricTitle": "メトリック", - "visTypeVislib.heatmap.groupTitle": "Y 軸", - "visTypeVislib.heatmap.metricTitle": "値", - "visTypeVislib.heatmap.segmentTitle": "X 軸", - "visTypeVislib.heatmap.splitTitle": "チャートを分割", "visTypeXy.histogram.groupTitle": "系列を分割", "visTypeXy.histogram.metricTitle": "Y 軸", "visTypeXy.histogram.radiusTitle": "点のサイズ", @@ -4453,27 +4468,12 @@ "visTypeXy.line.radiusTitle": "点のサイズ", "visTypeXy.line.segmentTitle": "X 軸", "visTypeXy.line.splitTitle": "チャートを分割", - "visTypeVislib.pie.metricTitle": "サイズのスライス", - "visTypeVislib.pie.pieTitle": "パイ", - "visTypeVislib.pie.segmentTitle": "スライスの分割", - "visTypeVislib.pie.splitTitle": "チャートを分割", "visTypeXy.scaleTypes.linearText": "線形", "visTypeXy.scaleTypes.logText": "ログ", "visTypeXy.scaleTypes.squareRootText": "平方根", "visTypeXy.thresholdLine.style.dashedText": "鎖線", "visTypeXy.thresholdLine.style.dotdashedText": "点線", "visTypeXy.thresholdLine.style.fullText": "完全", - "visTypeVislib.vislib.errors.noResultsFoundTitle": "結果が見つかりませんでした", - "visTypeVislib.vislib.heatmap.maxBucketsText": "定義された数列が多すぎます ({nr})。構成されている最大値は {max} です。", - "visTypeVislib.vislib.legend.filterForValueButtonAriaLabel": "値 {legendDataLabel} でフィルタリング", - "visTypeVislib.vislib.legend.filterOptionsLegend": "{legendDataLabel}、フィルターオプション", - "visTypeVislib.vislib.legend.filterOutValueButtonAriaLabel": "値 {legendDataLabel} を除外", - "visTypeVislib.vislib.legend.loadingLabel": "読み込み中…", - "visTypeVislib.vislib.legend.toggleLegendButtonAriaLabel": "凡例を切り替える", - "visTypeVislib.vislib.legend.toggleLegendButtonTitle": "凡例を切り替える", - "visTypeVislib.vislib.legend.toggleOptionsButtonAriaLabel": "{legendDataLabel}、トグルオプション", - "visTypeVislib.vislib.tooltip.fieldLabel": "フィールド", - "visTypeVislib.vislib.tooltip.valueLabel": "値", "visualizations.advancedSettings.visualizeEnableLabsText": "ユーザーが実験的なビジュアライゼーションを作成、表示、編集できるようになります。無効の場合、\n ユーザーは本番準備が整ったビジュアライゼーションのみを利用できます。", "visualizations.advancedSettings.visualizeEnableLabsTitle": "実験的なビジュアライゼーションを有効にする", "visualizations.disabledLabVisualizationMessage": "ラボビジュアライゼーションを表示するには、高度な設定でラボモードをオンにしてください。", @@ -7153,7 +7153,6 @@ "xpack.fleet.agentPolicy.confirmModalConfirmButtonLabel": "変更を保存してデプロイ", "xpack.fleet.agentPolicy.confirmModalDescription": "このアクションは元に戻せません。続行していいですか?", "xpack.fleet.agentPolicy.confirmModalTitle": "変更を保存してデプロイ", - "xpack.fleet.agentPolicy.linkedAgentCountText": "{count, plural, one {#件のエージェント} other {#件のエージェント}}", "xpack.fleet.agentPolicyActionMenu.buttonText": "アクション", "xpack.fleet.agentPolicyActionMenu.copyPolicyActionText": "ポリシーをコピー", "xpack.fleet.agentPolicyActionMenu.enrollAgentActionText": "エージェントの追加", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5377ae790c601..609e09d0197af 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -248,6 +248,7 @@ "charts.colormaps.redsText": "红色", "charts.colormaps.yellowToRedText": "黄到红", "charts.colorPicker.setColor.screenReaderDescription": "为值 {legendDataLabel} 设置颜色", + "charts.countText": "计数", "console.autocomplete.addMethodMetaText": "方法", "console.consoleDisplayName": "控制台", "console.consoleMenu.copyAsCurlMessage": "请求已复制为 cURL", @@ -1350,6 +1351,8 @@ "data.search.functions.kibana_context.savedSearchId.help": "指定要用于查询和筛选的已保存搜索 ID", "data.search.functions.kibana_context.timeRange.help": "指定 Kibana 时间范围筛选", "data.search.functions.kibana.help": "获取 kibana 全局上下文", + "data.triggers.applyFilterDescription": "应用 kibana 筛选时。可能是单个值或范围筛选。", + "data.triggers.applyFilterTitle": "应用筛选", "devTools.badge.readOnly.text": "只读", "devTools.badge.readOnly.tooltip": "无法保存", "devTools.devToolsTitle": "开发工具", @@ -1575,6 +1578,10 @@ "embeddableApi.samples.contactCard.displayName": "联系卡片", "embeddableApi.samples.filterableContainer.displayName": "可筛选仪表板", "embeddableApi.samples.filterableEmbeddable.displayName": "可筛选", + "embeddableApi.selectRangeTrigger.description": "可视化上的一组值", + "embeddableApi.selectRangeTrigger.title": "范围选择", + "embeddableApi.valueClickTrigger.description": "可视化上的数据点单击", + "embeddableApi.valueClickTrigger.title": "单击", "esUi.cronEditor.cronDaily.fieldHour.textAtLabel": "在", "esUi.cronEditor.cronDaily.fieldTimeLabel": "时间", "esUi.cronEditor.cronDaily.hourSelectLabel": "小时", @@ -3483,12 +3490,6 @@ "uiActions.actionPanel.more": "更多", "uiActions.actionPanel.title": "选项", "uiActions.errors.incompatibleAction": "操作不兼容", - "data.triggers.applyFilterDescription": "应用 kibana 筛选时。可能是单个值或范围筛选。", - "data.triggers.applyFilterTitle": "应用筛选", - "embeddableApi.selectRangeTrigger.description": "可视化上的一组值", - "embeddableApi.selectRangeTrigger.title": "范围选择", - "embeddableApi.valueClickTrigger.description": "可视化上的数据点单击", - "embeddableApi.valueClickTrigger.title": "单击", "usageCollection.stats.notReadyMessage": "统计信息尚未准备就绪。请稍后重试。", "visDefaultEditor.advancedToggle.advancedLinkLabel": "高级", "visDefaultEditor.agg.toggleEditorButtonAriaLabel": "切换 {schema} 编辑器", @@ -4307,27 +4308,6 @@ "visTypeVislib.advancedSettings.visualization.heatmap.maxBucketsText": "单个数据源可以返回的最大存储桶数目。较高的数目可能对浏览器呈现性能有负面影响", "visTypeVislib.advancedSettings.visualization.heatmap.maxBucketsTitle": "热图最大存储桶数", "visTypeVislib.aggResponse.allDocsTitle": "所有文档", - "visTypeXy.area.areaTitle": "面积图", - "charts.countText": "计数", - "visTypeXy.area.groupTitle": "拆分序列", - "visTypeXy.area.metricsTitle": "Y 轴", - "visTypeXy.area.radiusTitle": "点大小", - "visTypeXy.area.segmentTitle": "X 轴", - "visTypeXy.area.splitTitle": "拆分图表", - "visTypeXy.area.tabs.metricsAxesTitle": "指标和轴", - "visTypeXy.area.tabs.panelSettingsTitle": "面板设置", - "visTypeXy.axisModes.normalText": "正常", - "visTypeXy.axisModes.percentageText": "百分比", - "visTypeXy.axisModes.silhouetteText": "剪影", - "visTypeXy.axisModes.wiggleText": "扭动", - "visTypeXy.categoryAxis.rotate.angledText": "带角度", - "visTypeXy.categoryAxis.rotate.horizontalText": "水平", - "visTypeXy.categoryAxis.rotate.verticalText": "垂直", - "visTypeXy.chartModes.normalText": "正常", - "visTypeXy.chartModes.stackedText": "堆叠", - "visTypeXy.chartTypes.areaText": "面积图", - "visTypeXy.chartTypes.barText": "条形图", - "visTypeXy.chartTypes.lineText": "折线图", "visTypeVislib.controls.gaugeOptions.alignmentLabel": "对齐方式", "visTypeVislib.controls.gaugeOptions.autoExtendRangeLabel": "自动扩展范围", "visTypeVislib.controls.gaugeOptions.displayWarningsLabel": "显示警告", @@ -4353,6 +4333,68 @@ "visTypeVislib.controls.heatmapOptions.scaleToDataBoundsLabel": "缩放到数据边界", "visTypeVislib.controls.heatmapOptions.showLabelsTitle": "显示标签", "visTypeVislib.controls.heatmapOptions.useCustomRangesLabel": "使用定制范围", + "visTypeVislib.editors.heatmap.basicSettingsTitle": "基本设置", + "visTypeVislib.editors.heatmap.heatmapSettingsTitle": "热图设置", + "visTypeVislib.editors.heatmap.highlightLabel": "高亮范围", + "visTypeVislib.editors.heatmap.highlightLabelTooltip": "高亮显示图表中鼠标悬停的范围以及图例中对应的标签。", + "visTypeVislib.editors.pie.donutLabel": "圆环图", + "visTypeVislib.editors.pie.labelsSettingsTitle": "标签设置", + "visTypeVislib.editors.pie.pieSettingsTitle": "饼图设置", + "visTypeVislib.editors.pie.showLabelsLabel": "显示标签", + "visTypeVislib.editors.pie.showTopLevelOnlyLabel": "仅显示顶级", + "visTypeVislib.editors.pie.showValuesLabel": "显示值", + "visTypeVislib.functions.pie.help": "饼图可视化", + "visTypeVislib.functions.vislib.help": "Vislib 可视化", + "visTypeVislib.gauge.alignmentAutomaticTitle": "自动", + "visTypeVislib.gauge.alignmentHorizontalTitle": "水平", + "visTypeVislib.gauge.alignmentVerticalTitle": "垂直", + "visTypeVislib.gauge.gaugeTitle": "仪表盘图", + "visTypeVislib.gauge.gaugeTypes.arcText": "弧形", + "visTypeVislib.gauge.gaugeTypes.circleText": "圆形", + "visTypeVislib.gauge.groupTitle": "拆分组", + "visTypeVislib.gauge.metricTitle": "指标", + "visTypeVislib.goal.goalTitle": "目标图", + "visTypeVislib.goal.groupTitle": "拆分组", + "visTypeVislib.goal.metricTitle": "指标", + "visTypeVislib.heatmap.groupTitle": "Y 轴", + "visTypeVislib.heatmap.metricTitle": "值", + "visTypeVislib.heatmap.segmentTitle": "X 轴", + "visTypeVislib.heatmap.splitTitle": "拆分图表", + "visTypeVislib.pie.metricTitle": "切片大小", + "visTypeVislib.pie.pieTitle": "饼图", + "visTypeVislib.pie.segmentTitle": "拆分切片", + "visTypeVislib.pie.splitTitle": "拆分图表", + "visTypeVislib.vislib.errors.noResultsFoundTitle": "找不到结果", + "visTypeVislib.vislib.heatmap.maxBucketsText": "定义了过多的序列 ({nr})。配置的最大值为 {max}。", + "visTypeVislib.vislib.legend.filterForValueButtonAriaLabel": "筛留值 {legendDataLabel}", + "visTypeVislib.vislib.legend.filterOptionsLegend": "{legendDataLabel}, 筛选选项", + "visTypeVislib.vislib.legend.filterOutValueButtonAriaLabel": "筛除值 {legendDataLabel}", + "visTypeVislib.vislib.legend.loadingLabel": "正在加载……", + "visTypeVislib.vislib.legend.toggleLegendButtonAriaLabel": "切换图例", + "visTypeVislib.vislib.legend.toggleLegendButtonTitle": "切换图例", + "visTypeVislib.vislib.legend.toggleOptionsButtonAriaLabel": "{legendDataLabel}, 切换选项", + "visTypeVislib.vislib.tooltip.fieldLabel": "字段", + "visTypeVislib.vislib.tooltip.valueLabel": "值", + "visTypeXy.area.areaTitle": "面积图", + "visTypeXy.area.groupTitle": "拆分序列", + "visTypeXy.area.metricsTitle": "Y 轴", + "visTypeXy.area.radiusTitle": "点大小", + "visTypeXy.area.segmentTitle": "X 轴", + "visTypeXy.area.splitTitle": "拆分图表", + "visTypeXy.area.tabs.metricsAxesTitle": "指标和轴", + "visTypeXy.area.tabs.panelSettingsTitle": "面板设置", + "visTypeXy.axisModes.normalText": "正常", + "visTypeXy.axisModes.percentageText": "百分比", + "visTypeXy.axisModes.silhouetteText": "剪影", + "visTypeXy.axisModes.wiggleText": "扭动", + "visTypeXy.categoryAxis.rotate.angledText": "带角度", + "visTypeXy.categoryAxis.rotate.horizontalText": "水平", + "visTypeXy.categoryAxis.rotate.verticalText": "垂直", + "visTypeXy.chartModes.normalText": "正常", + "visTypeXy.chartModes.stackedText": "堆叠", + "visTypeXy.chartTypes.areaText": "面积图", + "visTypeXy.chartTypes.barText": "条形图", + "visTypeXy.chartTypes.lineText": "折线图", "visTypeXy.controls.pointSeries.categoryAxis.alignLabel": "对齐", "visTypeXy.controls.pointSeries.categoryAxis.filterLabelsLabel": "筛选标签", "visTypeXy.controls.pointSeries.categoryAxis.labelsTitle": "标签", @@ -4395,16 +4437,6 @@ "visTypeXy.controls.pointSeries.valueAxes.toggleOptionsAriaLabel": "切换 {axisName} 选项", "visTypeXy.controls.pointSeries.valueAxes.yAxisTitle": "Y 轴", "visTypeXy.controls.truncateLabel": "截断", - "visTypeVislib.editors.heatmap.basicSettingsTitle": "基本设置", - "visTypeVislib.editors.heatmap.heatmapSettingsTitle": "热图设置", - "visTypeVislib.editors.heatmap.highlightLabel": "高亮范围", - "visTypeVislib.editors.heatmap.highlightLabelTooltip": "高亮显示图表中鼠标悬停的范围以及图例中对应的标签。", - "visTypeVislib.editors.pie.donutLabel": "圆环图", - "visTypeVislib.editors.pie.labelsSettingsTitle": "标签设置", - "visTypeVislib.editors.pie.pieSettingsTitle": "饼图设置", - "visTypeVislib.editors.pie.showLabelsLabel": "显示标签", - "visTypeVislib.editors.pie.showTopLevelOnlyLabel": "仅显示顶级", - "visTypeVislib.editors.pie.showValuesLabel": "显示值", "visTypeXy.editors.pointSeries.currentTimeMarkerLabel": "当前时间标记", "visTypeXy.editors.pointSeries.orderBucketsBySumLabel": "按总计值排序存储桶", "visTypeXy.editors.pointSeries.settingsTitle": "设置", @@ -4415,23 +4447,6 @@ "visTypeXy.editors.pointSeries.thresholdLine.valueLabel": "阈值", "visTypeXy.editors.pointSeries.thresholdLine.widthLabel": "线条宽度", "visTypeXy.editors.pointSeries.thresholdLineSettingsTitle": "阈值线条", - "visTypeVislib.functions.pie.help": "饼图可视化", - "visTypeVislib.functions.vislib.help": "Vislib 可视化", - "visTypeVislib.gauge.alignmentAutomaticTitle": "自动", - "visTypeVislib.gauge.alignmentHorizontalTitle": "水平", - "visTypeVislib.gauge.alignmentVerticalTitle": "垂直", - "visTypeVislib.gauge.gaugeTitle": "仪表盘图", - "visTypeVislib.gauge.gaugeTypes.arcText": "弧形", - "visTypeVislib.gauge.gaugeTypes.circleText": "圆形", - "visTypeVislib.gauge.groupTitle": "拆分组", - "visTypeVislib.gauge.metricTitle": "指标", - "visTypeVislib.goal.goalTitle": "目标图", - "visTypeVislib.goal.groupTitle": "拆分组", - "visTypeVislib.goal.metricTitle": "指标", - "visTypeVislib.heatmap.groupTitle": "Y 轴", - "visTypeVislib.heatmap.metricTitle": "值", - "visTypeVislib.heatmap.segmentTitle": "X 轴", - "visTypeVislib.heatmap.splitTitle": "拆分图表", "visTypeXy.histogram.groupTitle": "拆分序列", "visTypeXy.histogram.metricTitle": "Y 轴", "visTypeXy.histogram.radiusTitle": "点大小", @@ -4455,27 +4470,12 @@ "visTypeXy.line.radiusTitle": "点大小", "visTypeXy.line.segmentTitle": "X 轴", "visTypeXy.line.splitTitle": "拆分图表", - "visTypeVislib.pie.metricTitle": "切片大小", - "visTypeVislib.pie.pieTitle": "饼图", - "visTypeVislib.pie.segmentTitle": "拆分切片", - "visTypeVislib.pie.splitTitle": "拆分图表", "visTypeXy.scaleTypes.linearText": "线性", "visTypeXy.scaleTypes.logText": "对数", "visTypeXy.scaleTypes.squareRootText": "平方根", "visTypeXy.thresholdLine.style.dashedText": "虚线", "visTypeXy.thresholdLine.style.dotdashedText": "点虚线", "visTypeXy.thresholdLine.style.fullText": "实线", - "visTypeVislib.vislib.errors.noResultsFoundTitle": "找不到结果", - "visTypeVislib.vislib.heatmap.maxBucketsText": "定义了过多的序列 ({nr})。配置的最大值为 {max}。", - "visTypeVislib.vislib.legend.filterForValueButtonAriaLabel": "筛留值 {legendDataLabel}", - "visTypeVislib.vislib.legend.filterOptionsLegend": "{legendDataLabel}, 筛选选项", - "visTypeVislib.vislib.legend.filterOutValueButtonAriaLabel": "筛除值 {legendDataLabel}", - "visTypeVislib.vislib.legend.loadingLabel": "正在加载……", - "visTypeVislib.vislib.legend.toggleLegendButtonAriaLabel": "切换图例", - "visTypeVislib.vislib.legend.toggleLegendButtonTitle": "切换图例", - "visTypeVislib.vislib.legend.toggleOptionsButtonAriaLabel": "{legendDataLabel}, 切换选项", - "visTypeVislib.vislib.tooltip.fieldLabel": "字段", - "visTypeVislib.vislib.tooltip.valueLabel": "值", "visualizations.advancedSettings.visualizeEnableLabsText": "允许用户创建、查看和编辑实验性可视化。如果禁用,\n 仅被视为生产就绪的可视化可供用户使用。", "visualizations.advancedSettings.visualizeEnableLabsTitle": "启用实验性可视化", "visualizations.disabledLabVisualizationMessage": "请在高级设置中打开实验室模式,以查看实验室可视化。", @@ -7160,7 +7160,6 @@ "xpack.fleet.agentPolicy.confirmModalConfirmButtonLabel": "保存并部署更改", "xpack.fleet.agentPolicy.confirmModalDescription": "此操作无法撤消。是否确定要继续?", "xpack.fleet.agentPolicy.confirmModalTitle": "保存并部署更改", - "xpack.fleet.agentPolicy.linkedAgentCountText": "{count, plural, one {# 个代理} other {# 个代理}}", "xpack.fleet.agentPolicyActionMenu.buttonText": "操作", "xpack.fleet.agentPolicyActionMenu.copyPolicyActionText": "复制策略", "xpack.fleet.agentPolicyActionMenu.enrollAgentActionText": "添加代理", diff --git a/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap index 1a18cf5651bee..21d65f63783c5 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap @@ -490,8 +490,9 @@ exports[`DonutChart component renders a donut chart 1`] = ` - Up + Down - Down + Up - Up + Down - Down + Up - - - - -`; diff --git a/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart_legend_row.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart_legend_row.test.tsx.snap index bc6033ea7109a..6e2a58cf528ed 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart_legend_row.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart_legend_row.test.tsx.snap @@ -15,6 +15,7 @@ exports[`DonutChartLegendRow passes appropriate props 1`] = ` Foo diff --git a/x-pack/plugins/uptime/public/components/common/charts/__tests__/donut_chart_legend.test.tsx b/x-pack/plugins/uptime/public/components/common/charts/__tests__/donut_chart_legend.test.tsx index 1b71b87884fb8..2ef02106e6e66 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/__tests__/donut_chart_legend.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/__tests__/donut_chart_legend.test.tsx @@ -3,14 +3,30 @@ * 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 { renderWithIntl } from '@kbn/test/jest'; import { DonutChartLegend } from '../donut_chart_legend'; -import { shallowWithIntl } from '@kbn/test/jest'; -import React from 'react'; + +import { STATUS_DOWN_LABEL, STATUS_UP_LABEL } from '../../translations'; describe('DonutChartLegend', () => { it('applies valid props as expected', () => { - const wrapper = shallowWithIntl(); - expect(wrapper).toMatchSnapshot(); + const up = 45; + const down = 23; + const component = renderWithIntl(); + + expect( + component.find('[data-test-subj="xpack.uptime.snapshot.donutChart.up.label"]').text() + ).toBe(STATUS_UP_LABEL); + expect(component.find('[data-test-subj="xpack.uptime.snapshot.donutChart.up"]').text()).toBe( + `${up}` + ); + expect( + component.find('[data-test-subj="xpack.uptime.snapshot.donutChart.down.label"]').text() + ).toBe(STATUS_DOWN_LABEL); + expect(component.find('[data-test-subj="xpack.uptime.snapshot.donutChart.down"]').text()).toBe( + `${down}` + ); }); }); diff --git a/x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx b/x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx index f3b50895fff63..92b9c72e3f1e6 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx @@ -34,14 +34,14 @@ export const DonutChartLegend = ({ down, up }: Props) => { diff --git a/x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend_row.tsx b/x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend_row.tsx index fc67a86db3b48..0f637aff3bfa4 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend_row.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend_row.tsx @@ -31,7 +31,7 @@ export const DonutChartLegendRow = ({ color, content, message, 'data-test-subj': - + {message} diff --git a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts index 52c9dd74167f5..b160677673331 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts @@ -99,21 +99,24 @@ export default function ApiTest({ getService }: FtrProviderContext) { Array [ Object { "avgResponseTime": Object { - "value": 556200.153101878, + "value": 420419.34550767, }, "transactionErrorRate": Object { "value": 0, }, "transactionsPerMinute": Object { - "value": 117.133333333333, + "value": 45.6333333333333, }, }, Object { "avgResponseTime": Object { - "value": 2629229.16666667, + "value": 2382833.33333333, + }, + "transactionErrorRate": Object { + "value": null, }, "transactionsPerMinute": Object { - "value": 3.2, + "value": 0.2, }, }, Object { @@ -151,24 +154,24 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, Object { "avgResponseTime": Object { - "value": 563605.417040359, + "value": 24920.1052631579, }, "transactionErrorRate": Object { "value": 0.0210526315789474, }, "transactionsPerMinute": Object { - "value": 7.43333333333333, + "value": 3.16666666666667, }, }, Object { "avgResponseTime": Object { - "value": 217138.013645224, + "value": 29542.6607142857, }, "transactionErrorRate": Object { - "value": 0.315789473684211, + "value": 0.0357142857142857, }, "transactionsPerMinute": Object { - "value": 17.1, + "value": 1.86666666666667, }, }, Object { @@ -186,6 +189,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { "avgResponseTime": Object { "value": 2319812.5, }, + "transactionErrorRate": Object { + "value": null, + }, "transactionsPerMinute": Object { "value": 0.533333333333333, }, diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts b/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts index 7e970493eb611..50c261d2d37ad 100644 --- a/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts +++ b/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts @@ -49,7 +49,7 @@ export default function rumServicesApiTests({ getService }: FtrProviderContext) expectSnapshot(response.body).toMatchInline(` Object { - "cls": "0.000", + "cls": 0, "clsRanks": Array [ 100, 0, diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/find_statuses.ts b/x-pack/test/detection_engine_api_integration/basic/tests/find_statuses.ts index fe80402b60731..785b74d334276 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/find_statuses.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/find_statuses.ts @@ -15,7 +15,7 @@ import { deleteAllRulesStatuses, getSimpleRule, createRule, - waitForRuleSuccess, + waitForRuleSuccessOrStatus, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -47,7 +47,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should return a single rule status when a single rule is loaded from a find status with defaults added', async () => { const resBody = await createRule(supertest, getSimpleRule('rule-1', true)); - await waitForRuleSuccess(supertest, resBody.id); + await waitForRuleSuccessOrStatus(supertest, resBody.id); // query the single rule from _find const { body } = await supertest diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts b/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts index f8a25b0081ef9..2e00be6f77061 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts @@ -23,7 +23,7 @@ import { createRule, waitForSignalsToBePresent, getSignalsByIds, - waitForRuleSuccess, + waitForRuleSuccessOrStatus, getRuleForSignalTesting, } from '../../utils'; @@ -79,7 +79,7 @@ export default ({ getService }: FtrProviderContext) => { it('should be able to execute and get 10 signals', async () => { const rule = getRuleForSignalTesting(['auditbeat-*']); const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 10, [id]); const signalsOpen = await getSignalsByIds(supertest, [id]); expect(signalsOpen.hits.hits.length).equal(10); @@ -88,7 +88,7 @@ export default ({ getService }: FtrProviderContext) => { it('should be have set the signals in an open state initially', async () => { const rule = getRuleForSignalTesting(['auditbeat-*']); const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 10, [id]); const signalsOpen = await getSignalsByIds(supertest, [id]); const everySignalOpen = signalsOpen.hits.hits.every( @@ -104,7 +104,7 @@ export default ({ getService }: FtrProviderContext) => { it('should be able to get a count of 10 closed signals when closing 10', async () => { const rule = getRuleForSignalTesting(['auditbeat-*']); const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 10, [id]); const signalsOpen = await getSignalsByIds(supertest, [id]); const signalIds = signalsOpen.hits.hits.map((signal) => signal._id); @@ -131,7 +131,7 @@ export default ({ getService }: FtrProviderContext) => { it('should be able close 10 signals immediately and they all should be closed', async () => { const rule = getRuleForSignalTesting(['auditbeat-*']); const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 10, [id]); const signalsOpen = await getSignalsByIds(supertest, [id]); const signalIds = signalsOpen.hits.hits.map((signal) => signal._id); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts index bbd85e353e095..a2c3fc6c6c288 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts @@ -17,7 +17,7 @@ import { getWebHookAction, getRuleWithWebHookAction, getSimpleRuleOutputWithWebHookAction, - waitForRuleSuccess, + waitForRuleSuccessOrStatus, createRule, } from '../../utils'; @@ -60,7 +60,7 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); const rule = await createRule(supertest, getRuleWithWebHookAction(hookAction.id, true)); - await waitForRuleSuccess(supertest, rule.id); + await waitForRuleSuccessOrStatus(supertest, rule.id); // expected result for status should be 'succeeded' const { body } = await supertest @@ -86,7 +86,7 @@ export default ({ getService }: FtrProviderContext) => { }; const rule = await createRule(supertest, ruleWithAction); - await waitForRuleSuccess(supertest, rule.id); + await waitForRuleSuccessOrStatus(supertest, rule.id); // expected result for status should be 'succeeded' const { body } = await supertest diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts index 7e4a6ad86cda5..b90bea66be11f 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts @@ -26,7 +26,7 @@ import { removeServerGeneratedProperties, downgradeImmutableRule, createRule, - waitForRuleSuccess, + waitForRuleSuccessOrStatus, installPrePackagedRules, getRule, createExceptionList, @@ -113,7 +113,7 @@ export default ({ getService }: FtrProviderContext) => { }; const rule = await createRule(supertest, ruleWithException); - await waitForRuleSuccess(supertest, rule.id); + await waitForRuleSuccessOrStatus(supertest, rule.id); const bodyToCompare = removeServerGeneratedProperties(rule); const expected: Partial = { @@ -444,7 +444,7 @@ export default ({ getService }: FtrProviderContext) => { ], }; const { id: createdId } = await createRule(supertest, ruleWithException); - await waitForRuleSuccess(supertest, createdId); + await waitForRuleSuccessOrStatus(supertest, createdId); await waitForSignalsToBePresent(supertest, 10, [createdId]); const signalsOpen = await getSignalsByIds(supertest, [createdId]); expect(signalsOpen.hits.hits.length).equal(10); @@ -490,7 +490,7 @@ export default ({ getService }: FtrProviderContext) => { ], }; const rule = await createRule(supertest, ruleWithException); - await waitForRuleSuccess(supertest, rule.id); + await waitForRuleSuccessOrStatus(supertest, rule.id); const signalsOpen = await getSignalsByIds(supertest, [rule.id]); expect(signalsOpen.hits.hits.length).equal(0); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts index 0da12ebba055a..0cde7bf9a22fc 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts @@ -24,13 +24,16 @@ import { removeServerGeneratedPropertiesIncludingRuleId, getSimpleMlRule, getSimpleMlRuleOutput, - waitForRuleSuccess, + waitForRuleSuccessOrStatus, + waitForSignalsToBePresent, getRuleForSignalTesting, + getRuleForSignalTestingWithTimestampOverride, } from '../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); describe('create_rules', () => { describe('validation errors', () => { @@ -97,7 +100,7 @@ export default ({ getService }: FtrProviderContext) => { .send(simpleRule) .expect(200); - await waitForRuleSuccess(supertest, body.id); + await waitForRuleSuccessOrStatus(supertest, body.id); const { body: statusBody } = await supertest .post(DETECTION_ENGINE_RULES_STATUS_URL) @@ -201,5 +204,46 @@ export default ({ getService }: FtrProviderContext) => { }); }); }); + describe('missing timestamps', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + // to edit these files run the following script + // cd $HOME/kibana/x-pack && nvm use && node ../scripts/es_archiver edit security_solution/timestamp_override + await esArchiver.load('security_solution/timestamp_override'); + }); + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + await esArchiver.unload('security_solution/timestamp_override'); + }); + it('should create a single rule which has a timestamp override and generates two signals with a failing status', async () => { + // should be a failing status because one of the indices in the index pattern is missing + // the timestamp override field. + + // defaults to event.ingested timestamp override. + // event.ingested is one of the timestamp fields set on the es archive data + // inside of x-pack/test/functional/es_archives/security_solution/timestamp_override/data.json.gz + const simpleRule = getRuleForSignalTestingWithTimestampOverride(['myfa*']); + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(simpleRule) + .expect(200); + const bodyId = body.id; + + await waitForRuleSuccessOrStatus(supertest, bodyId, 'failed'); + await waitForSignalsToBePresent(supertest, 2, [bodyId]); + + const { body: statusBody } = await supertest + .post(DETECTION_ENGINE_RULES_STATUS_URL) + .set('kbn-xsrf', 'true') + .send({ ids: [bodyId] }) + .expect(200); + + // set to "failed" for now. Will update this with a partial failure + // once I figure out the logic + expect(statusBody[bodyId].current_status.status).to.eql('failed'); + }); + }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts index 7ea47312a5030..2577c6b163604 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts @@ -22,7 +22,7 @@ import { getSimpleRuleWithoutRuleId, removeServerGeneratedProperties, removeServerGeneratedPropertiesIncludingRuleId, - waitForRuleSuccess, + waitForRuleSuccessOrStatus, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -99,7 +99,7 @@ export default ({ getService }: FtrProviderContext): void => { .send([simpleRule]) .expect(200); - await waitForRuleSuccess(supertest, body[0].id); + await waitForRuleSuccessOrStatus(supertest, body[0].id); const { body: statusBody } = await supertest .post(DETECTION_ENGINE_RULES_STATUS_URL) diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts index 21cfab3db6d6a..1f7deddbd5e76 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -19,7 +19,7 @@ import { deleteSignalsIndex, getSignalsByIds, removeServerGeneratedProperties, - waitForRuleSuccess, + waitForRuleSuccessOrStatus, waitForSignalsToBePresent, } from '../../utils'; @@ -72,7 +72,7 @@ export default ({ getService }: FtrProviderContext) => { supertest, getCreateThreatMatchRulesSchemaMock('rule-1', true) ); - await waitForRuleSuccess(supertest, ruleResponse.id); + await waitForRuleSuccessOrStatus(supertest, ruleResponse.id); const { body: statusBody } = await supertest .post(DETECTION_ENGINE_RULES_STATUS_URL) @@ -128,7 +128,7 @@ export default ({ getService }: FtrProviderContext) => { }; const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 10, [id]); const signalsOpen = await getSignalsByIds(supertest, [id]); expect(signalsOpen.hits.hits.length).equal(10); @@ -163,7 +163,7 @@ export default ({ getService }: FtrProviderContext) => { }; const ruleResponse = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, ruleResponse.id); + await waitForRuleSuccessOrStatus(supertest, ruleResponse.id); const signalsOpen = await getSignalsByIds(supertest, [ruleResponse.id]); expect(signalsOpen.hits.hits.length).equal(0); }); @@ -201,7 +201,7 @@ export default ({ getService }: FtrProviderContext) => { }; const ruleResponse = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, ruleResponse.id); + await waitForRuleSuccessOrStatus(supertest, ruleResponse.id); const signalsOpen = await getSignalsByIds(supertest, [ruleResponse.id]); expect(signalsOpen.hits.hits.length).equal(0); }); @@ -239,7 +239,7 @@ export default ({ getService }: FtrProviderContext) => { }; const ruleResponse = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, ruleResponse.id); + await waitForRuleSuccessOrStatus(supertest, ruleResponse.id); const signalsOpen = await getSignalsByIds(supertest, [ruleResponse.id]); expect(signalsOpen.hits.hits.length).equal(0); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/date.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/date.ts index 09cc470defa08..4271ce9b37ebb 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/date.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/date.ts @@ -21,7 +21,7 @@ import { deleteSignalsIndex, getRuleForSignalTesting, getSignalsById, - waitForRuleSuccess, + waitForRuleSuccessOrStatus, waitForSignalsToBePresent, } from '../../../utils'; @@ -50,7 +50,7 @@ export default ({ getService }: FtrProviderContext) => { it('should find all the dates from the data set when no exceptions are set on the rule', async () => { const rule = getRuleForSignalTesting(['date']); const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); @@ -74,7 +74,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); @@ -105,7 +105,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); @@ -140,7 +140,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); @@ -183,7 +183,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); expect(hits).to.eql([]); @@ -203,7 +203,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); expect(hits).to.eql([]); @@ -221,7 +221,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); @@ -248,7 +248,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); expect(hits).to.eql([]); @@ -268,7 +268,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); @@ -291,7 +291,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); @@ -314,7 +314,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); @@ -338,7 +338,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); expect(hits).to.eql([]); @@ -358,7 +358,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); expect(hits).to.eql([]); @@ -376,7 +376,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); @@ -396,7 +396,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); expect(hits).to.eql([]); @@ -415,7 +415,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); @@ -445,7 +445,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); @@ -477,7 +477,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); @@ -510,7 +510,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); expect(hits).to.eql([]); @@ -534,7 +534,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); @@ -562,7 +562,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); @@ -595,7 +595,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.date).sort(); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/double.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/double.ts index a5793489cd8d0..158e17299fe9f 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/double.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/double.ts @@ -21,7 +21,7 @@ import { deleteSignalsIndex, getRuleForSignalTesting, getSignalsById, - waitForRuleSuccess, + waitForRuleSuccessOrStatus, waitForSignalsToBePresent, } from '../../../utils'; @@ -52,7 +52,7 @@ export default ({ getService }: FtrProviderContext) => { it('should find all the double from the data set when no exceptions are set on the rule', async () => { const rule = getRuleForSignalTesting(['double']); const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); @@ -71,7 +71,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); @@ -98,7 +98,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); @@ -133,7 +133,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); @@ -176,7 +176,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); expect(hits).to.eql([]); @@ -196,7 +196,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); expect(hits).to.eql([]); @@ -214,7 +214,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); @@ -241,7 +241,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); expect(hits).to.eql([]); @@ -261,7 +261,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); @@ -280,7 +280,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); @@ -299,7 +299,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); @@ -318,7 +318,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); expect(hits).to.eql([]); @@ -338,7 +338,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); expect(hits).to.eql([]); @@ -356,7 +356,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); @@ -376,7 +376,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); expect(hits).to.eql([]); @@ -395,7 +395,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); @@ -421,7 +421,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); @@ -444,7 +444,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); @@ -467,7 +467,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); expect(hits).to.eql([]); @@ -491,7 +491,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); @@ -514,7 +514,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); @@ -537,7 +537,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); expect(hits).to.eql([]); @@ -562,7 +562,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); @@ -589,7 +589,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); @@ -612,7 +612,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); @@ -635,7 +635,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); @@ -660,7 +660,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); @@ -683,7 +683,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); @@ -706,7 +706,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); @@ -732,7 +732,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/float.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/float.ts index 955d27c086466..0bea2d73151f2 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/float.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/float.ts @@ -21,7 +21,7 @@ import { deleteSignalsIndex, getRuleForSignalTesting, getSignalsById, - waitForRuleSuccess, + waitForRuleSuccessOrStatus, waitForSignalsToBePresent, } from '../../../utils'; @@ -52,7 +52,7 @@ export default ({ getService }: FtrProviderContext) => { it('should find all the float from the data set when no exceptions are set on the rule', async () => { const rule = getRuleForSignalTesting(['float']); const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); @@ -71,7 +71,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); @@ -98,7 +98,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); @@ -133,7 +133,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); @@ -176,7 +176,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); expect(hits).to.eql([]); @@ -196,7 +196,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); expect(hits).to.eql([]); @@ -214,7 +214,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); @@ -241,7 +241,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); expect(hits).to.eql([]); @@ -261,7 +261,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); @@ -280,7 +280,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); @@ -299,7 +299,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); @@ -318,7 +318,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); expect(hits).to.eql([]); @@ -338,7 +338,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); expect(hits).to.eql([]); @@ -356,7 +356,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); @@ -376,7 +376,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); expect(hits).to.eql([]); @@ -395,7 +395,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); @@ -421,7 +421,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); @@ -444,7 +444,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); @@ -467,7 +467,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); expect(hits).to.eql([]); @@ -491,7 +491,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); @@ -514,7 +514,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); @@ -537,7 +537,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); expect(hits).to.eql([]); @@ -559,7 +559,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); @@ -586,7 +586,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); @@ -609,7 +609,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); @@ -632,7 +632,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); @@ -657,7 +657,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); @@ -680,7 +680,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); @@ -703,7 +703,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); @@ -726,7 +726,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/integer.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/integer.ts index a1275afe288bf..600c1a609a694 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/integer.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/integer.ts @@ -21,7 +21,7 @@ import { deleteSignalsIndex, getRuleForSignalTesting, getSignalsById, - waitForRuleSuccess, + waitForRuleSuccessOrStatus, waitForSignalsToBePresent, } from '../../../utils'; @@ -52,7 +52,7 @@ export default ({ getService }: FtrProviderContext) => { it('should find all the integer from the data set when no exceptions are set on the rule', async () => { const rule = getRuleForSignalTesting(['integer']); const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); @@ -71,7 +71,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); @@ -98,7 +98,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); @@ -133,7 +133,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); @@ -176,7 +176,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); expect(hits).to.eql([]); @@ -196,7 +196,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); expect(hits).to.eql([]); @@ -214,7 +214,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); @@ -241,7 +241,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); expect(hits).to.eql([]); @@ -261,7 +261,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); @@ -280,7 +280,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); @@ -299,7 +299,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); @@ -318,7 +318,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); expect(hits).to.eql([]); @@ -338,7 +338,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); expect(hits).to.eql([]); @@ -356,7 +356,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); @@ -376,7 +376,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); expect(hits).to.eql([]); @@ -395,7 +395,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); @@ -421,7 +421,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); @@ -444,7 +444,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); @@ -467,7 +467,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); expect(hits).to.eql([]); @@ -491,7 +491,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); @@ -514,7 +514,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); @@ -537,7 +537,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); expect(hits).to.eql([]); @@ -559,7 +559,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); @@ -586,7 +586,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); @@ -609,7 +609,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); @@ -632,7 +632,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); @@ -657,7 +657,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); @@ -680,7 +680,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); @@ -703,7 +703,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); @@ -726,7 +726,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip.ts index 311354c63ca4a..bcdebed3dd45b 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip.ts @@ -21,7 +21,7 @@ import { deleteSignalsIndex, getRuleForSignalTesting, getSignalsById, - waitForRuleSuccess, + waitForRuleSuccessOrStatus, waitForSignalsToBePresent, } from '../../../utils'; @@ -50,7 +50,7 @@ export default ({ getService }: FtrProviderContext) => { it('should find all the ips from the data set when no exceptions are set on the rule', async () => { const rule = getRuleForSignalTesting(['ip']); const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -69,7 +69,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -96,7 +96,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -131,7 +131,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -174,7 +174,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); expect(ips).to.eql([]); @@ -192,7 +192,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -213,7 +213,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); expect(ips).to.eql([]); @@ -231,7 +231,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -258,7 +258,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); expect(ips).to.eql([]); @@ -278,7 +278,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -297,7 +297,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -316,7 +316,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -335,7 +335,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); expect(ips).to.eql([]); @@ -355,7 +355,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); expect(ips).to.eql([]); @@ -373,7 +373,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -393,7 +393,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); expect(ips).to.eql([]); @@ -412,7 +412,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -437,7 +437,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -460,7 +460,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -488,7 +488,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); expect(ips).to.eql([]); @@ -514,7 +514,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -541,7 +541,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -570,7 +570,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -595,7 +595,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -618,7 +618,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -646,7 +646,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -673,7 +673,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -700,7 +700,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts index 8f4827ec6e71c..9d6f1f2fb297a 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts @@ -21,7 +21,7 @@ import { deleteSignalsIndex, getRuleForSignalTesting, getSignalsById, - waitForRuleSuccess, + waitForRuleSuccessOrStatus, waitForSignalsToBePresent, } from '../../../utils'; @@ -50,7 +50,7 @@ export default ({ getService }: FtrProviderContext) => { it('should find all the ips from the data set when no exceptions are set on the rule', async () => { const rule = getRuleForSignalTesting(['ip_as_array']); const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -74,7 +74,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -105,7 +105,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -140,7 +140,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -159,7 +159,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -182,7 +182,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -203,7 +203,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); expect(ips).to.eql([]); @@ -221,7 +221,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -246,7 +246,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -273,7 +273,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); expect(ips).to.eql([]); @@ -293,7 +293,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -316,7 +316,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -335,7 +335,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -356,7 +356,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); expect(ips).to.eql([]); @@ -374,7 +374,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -397,7 +397,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); expect(ips).to.eql([[]]); @@ -416,7 +416,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -445,7 +445,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -472,7 +472,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -500,7 +500,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); expect(ips).to.eql([[]]); @@ -536,7 +536,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -567,7 +567,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -592,7 +592,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -615,7 +615,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -646,7 +646,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -687,7 +687,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); @@ -721,7 +721,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword.ts index e4e80cb1b65ea..a0183ad794a2f 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword.ts @@ -21,7 +21,7 @@ import { deleteSignalsIndex, getRuleForSignalTesting, getSignalsById, - waitForRuleSuccess, + waitForRuleSuccessOrStatus, waitForSignalsToBePresent, } from '../../../utils'; @@ -50,7 +50,7 @@ export default ({ getService }: FtrProviderContext) => { it('should find all the keyword from the data set when no exceptions are set on the rule', async () => { const rule = getRuleForSignalTesting(['keyword']); const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -69,7 +69,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -96,7 +96,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -131,7 +131,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -174,7 +174,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); expect(hits).to.eql([]); @@ -194,7 +194,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); expect(hits).to.eql([]); @@ -212,7 +212,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -239,7 +239,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); expect(hits).to.eql([]); @@ -259,7 +259,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -278,7 +278,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -297,7 +297,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -316,7 +316,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); expect(hits).to.eql([]); @@ -336,7 +336,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); expect(hits).to.eql([]); @@ -354,7 +354,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -374,7 +374,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); expect(hits).to.eql([]); @@ -393,7 +393,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -428,7 +428,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -451,7 +451,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -474,7 +474,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -502,7 +502,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); expect(hits).to.eql([]); @@ -526,7 +526,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -549,7 +549,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -577,7 +577,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts index 01e301c350851..81ea04de5def0 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts @@ -21,7 +21,7 @@ import { deleteSignalsIndex, getRuleForSignalTesting, getSignalsById, - waitForRuleSuccess, + waitForRuleSuccessOrStatus, waitForSignalsToBePresent, } from '../../../utils'; @@ -50,7 +50,7 @@ export default ({ getService }: FtrProviderContext) => { it('should find all the keyword from the data set when no exceptions are set on the rule', async () => { const rule = getRuleForSignalTesting(['keyword_as_array']); const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -74,7 +74,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -105,7 +105,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -140,7 +140,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -161,7 +161,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); expect(hits).to.eql([]); @@ -179,7 +179,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -206,7 +206,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); expect(hits).to.eql([]); @@ -226,7 +226,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -249,7 +249,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -268,7 +268,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -289,7 +289,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); expect(hits).to.eql([]); @@ -307,7 +307,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -330,7 +330,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); expect(hits).to.eql([[]]); @@ -349,7 +349,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -388,7 +388,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -426,7 +426,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -453,7 +453,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -480,7 +480,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -508,7 +508,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); expect(hits).to.eql([[]]); @@ -532,7 +532,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -555,7 +555,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -578,7 +578,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); @@ -609,7 +609,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/long.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/long.ts index ee52c41bc78e8..56667dbca925e 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/long.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/long.ts @@ -21,7 +21,7 @@ import { deleteSignalsIndex, getRuleForSignalTesting, getSignalsById, - waitForRuleSuccess, + waitForRuleSuccessOrStatus, waitForSignalsToBePresent, } from '../../../utils'; @@ -52,7 +52,7 @@ export default ({ getService }: FtrProviderContext) => { it('should find all the long from the data set when no exceptions are set on the rule', async () => { const rule = getRuleForSignalTesting(['long']); const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); @@ -71,7 +71,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); @@ -98,7 +98,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); @@ -133,7 +133,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); @@ -176,7 +176,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); expect(hits).to.eql([]); @@ -196,7 +196,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); expect(hits).to.eql([]); @@ -214,7 +214,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); @@ -241,7 +241,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); expect(hits).to.eql([]); @@ -261,7 +261,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); @@ -280,7 +280,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); @@ -299,7 +299,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); @@ -318,7 +318,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); expect(hits).to.eql([]); @@ -338,7 +338,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); expect(hits).to.eql([]); @@ -356,7 +356,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); @@ -376,7 +376,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); expect(hits).to.eql([]); @@ -395,7 +395,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); @@ -421,7 +421,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); @@ -444,7 +444,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); @@ -467,7 +467,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); expect(hits).to.eql([]); @@ -491,7 +491,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); @@ -514,7 +514,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); @@ -537,7 +537,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); expect(hits).to.eql([]); @@ -559,7 +559,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); @@ -586,7 +586,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); @@ -609,7 +609,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); @@ -632,7 +632,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); @@ -657,7 +657,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); @@ -680,7 +680,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); @@ -703,7 +703,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); @@ -726,7 +726,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts index 095d885149389..74507fc030e68 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts @@ -22,7 +22,7 @@ import { deleteSignalsIndex, getRuleForSignalTesting, getSignalsById, - waitForRuleSuccess, + waitForRuleSuccessOrStatus, waitForSignalsToBePresent, } from '../../../utils'; @@ -53,7 +53,7 @@ export default ({ getService }: FtrProviderContext) => { it('should find all the text from the data set when no exceptions are set on the rule', async () => { const rule = getRuleForSignalTesting(['text']); const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -72,7 +72,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -99,7 +99,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -134,7 +134,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -177,7 +177,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); expect(hits).to.eql([]); @@ -195,7 +195,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -214,7 +214,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); expect(hits).to.eql([]); @@ -232,7 +232,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -253,7 +253,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); expect(hits).to.eql([]); @@ -271,7 +271,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -298,7 +298,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); expect(hits).to.eql([]); @@ -316,7 +316,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -335,7 +335,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); expect(hits).to.eql(['word four', 'word one', 'word three', 'word two']); @@ -353,7 +353,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -374,7 +374,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -393,7 +393,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -412,7 +412,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -431,7 +431,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); expect(hits).to.eql([]); @@ -451,7 +451,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); expect(hits).to.eql([]); @@ -469,7 +469,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -489,7 +489,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); expect(hits).to.eql([]); @@ -508,7 +508,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -534,7 +534,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -557,7 +557,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -585,7 +585,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); expect(hits).to.eql([]); @@ -609,7 +609,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -637,7 +637,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -660,7 +660,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -688,7 +688,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); expect(hits).to.eql([]); @@ -714,7 +714,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -737,7 +737,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -765,7 +765,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -790,7 +790,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -818,7 +818,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -841,7 +841,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -869,7 +869,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts index ed63f1a0db25f..9a77cee6be1eb 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts @@ -21,7 +21,7 @@ import { deleteSignalsIndex, getRuleForSignalTesting, getSignalsById, - waitForRuleSuccess, + waitForRuleSuccessOrStatus, waitForSignalsToBePresent, } from '../../../utils'; @@ -50,7 +50,7 @@ export default ({ getService }: FtrProviderContext) => { it('should find all the text from the data set when no exceptions are set on the rule', async () => { const rule = getRuleForSignalTesting(['text_as_array']); const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -74,7 +74,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -105,7 +105,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -140,7 +140,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -161,7 +161,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); expect(hits).to.eql([]); @@ -179,7 +179,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -206,7 +206,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); expect(hits).to.eql([]); @@ -226,7 +226,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -249,7 +249,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -268,7 +268,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -289,7 +289,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); expect(hits).to.eql([]); @@ -307,7 +307,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -330,7 +330,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); expect(hits).to.eql([[]]); @@ -349,7 +349,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -388,7 +388,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -426,7 +426,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -453,7 +453,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -480,7 +480,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -508,7 +508,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); expect(hits).to.eql([[]]); @@ -532,7 +532,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -555,7 +555,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -578,7 +578,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); @@ -604,7 +604,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], ]); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_statuses.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_statuses.ts index 8bb4c45d91bdd..dfec35e4a64f3 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_statuses.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_statuses.ts @@ -14,7 +14,7 @@ import { deleteSignalsIndex, deleteAllRulesStatuses, getSimpleRule, - waitForRuleSuccess, + waitForRuleSuccessOrStatus, createRule, } from '../../utils'; @@ -66,7 +66,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should return a single rule status when a single rule is loaded from a find status with defaults added', async () => { const resBody = await createRule(supertest, getSimpleRule('rule-1', true)); - await waitForRuleSuccess(supertest, resBody.id); + await waitForRuleSuccessOrStatus(supertest, resBody.id); // query the single rule from _find const { body } = await supertest diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts index 64ee42fdb3f3e..34f7074326550 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts @@ -22,7 +22,7 @@ import { getSignalsByIds, getSignalsByRuleIds, getSimpleRule, - waitForRuleSuccess, + waitForRuleSuccessOrStatus, waitForSignalsToBePresent, } from '../../utils'; import { SIGNALS_TEMPLATE_VERSION } from '../../../../plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template'; @@ -63,7 +63,7 @@ export default ({ getService }: FtrProviderContext) => { query: `_id:${ID}`, }; const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsByIds(supertest, [id]); expect(signalsOpen.hits.hits.length).greaterThan(0); @@ -75,7 +75,7 @@ export default ({ getService }: FtrProviderContext) => { query: `_id:${ID}`, }; const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsByIds(supertest, [id]); expect(signalsOpen.hits.hits[0]._source.signal.rule.rule_id).eql(getSimpleRule().rule_id); @@ -87,7 +87,7 @@ export default ({ getService }: FtrProviderContext) => { query: `_id:${ID}`, }; const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsByIds(supertest, [id]); // remove rule to cut down on touch points for test changes when the rule format changes @@ -136,7 +136,7 @@ export default ({ getService }: FtrProviderContext) => { query: `_id:${ID}`, }; const { id: createdId } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, createdId); + await waitForRuleSuccessOrStatus(supertest, createdId); await waitForSignalsToBePresent(supertest, 1, [createdId]); // Run signals on top of that 1 signal which should create a single signal (on top of) a signal @@ -146,7 +146,7 @@ export default ({ getService }: FtrProviderContext) => { }; const { id } = await createRule(supertest, ruleForSignals); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); // Get our single signal on top of a signal @@ -201,7 +201,8 @@ export default ({ getService }: FtrProviderContext) => { }); }); - describe('EQL Rules', () => { + // ES PROMOTION FAILURE: http://github.com/elastic/kibana/issues/86709 + describe.skip('EQL Rules', () => { it('generates signals from EQL sequences in the expected form', async () => { const rule: EqlCreateSchema = { ...getRuleForSignalTesting(['auditbeat-*']), @@ -211,7 +212,7 @@ export default ({ getService }: FtrProviderContext) => { query: 'sequence by host.name [any where true] [any where true]', }; const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signals = await getSignalsByRuleIds(supertest, ['eql-rule']); const signal = signals.hits.hits[0]._source.signal; @@ -266,7 +267,7 @@ export default ({ getService }: FtrProviderContext) => { query: 'sequence by host.name [any where true] [any where true]', }; const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsByRuleIds(supertest, ['eql-rule']); const sequenceSignal = signalsOpen.hits.hits.find( @@ -354,7 +355,7 @@ export default ({ getService }: FtrProviderContext) => { }; const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsByIds(supertest, [id]); expect(signalsOpen.hits.hits.length).greaterThan(0); @@ -367,7 +368,7 @@ export default ({ getService }: FtrProviderContext) => { }; const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsByIds(supertest, [id]); expect(signalsOpen.hits.hits[0]._source.signal.rule.rule_id).eql(getSimpleRule().rule_id); @@ -379,7 +380,7 @@ export default ({ getService }: FtrProviderContext) => { query: '_id:1', }; const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsByIds(supertest, [id]); // remove rule to cut down on touch points for test changes when the rule format changes @@ -423,7 +424,7 @@ export default ({ getService }: FtrProviderContext) => { query: '_id:1', }; const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); // Run signals on top of that 1 signal which should create a single signal (on top of) a signal @@ -432,7 +433,7 @@ export default ({ getService }: FtrProviderContext) => { rule_id: 'signal-on-signal', }; const { id: createdId } = await createRule(supertest, ruleForSignals); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [createdId]); // Get our single signal on top of a signal @@ -507,7 +508,7 @@ export default ({ getService }: FtrProviderContext) => { query: '_id:1', }; const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsByIds(supertest, [id]); expect(signalsOpen.hits.hits.length).greaterThan(0); @@ -519,7 +520,7 @@ export default ({ getService }: FtrProviderContext) => { query: '_id:1', }; const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsByIds(supertest, [id]); expect(signalsOpen.hits.hits[0]._source.signal.rule.rule_id).eql(getSimpleRule().rule_id); @@ -531,7 +532,7 @@ export default ({ getService }: FtrProviderContext) => { query: '_id:1', }; const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsByIds(supertest, [id]); // remove rule to cut down on touch points for test changes when the rule format changes @@ -581,7 +582,7 @@ export default ({ getService }: FtrProviderContext) => { query: '_id:1', }; const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); // Run signals on top of that 1 signal which should create a single signal (on top of) a signal @@ -590,7 +591,7 @@ export default ({ getService }: FtrProviderContext) => { rule_id: 'signal-on-signal', }; const { id: createdId } = await createRule(supertest, ruleForSignals); - await waitForRuleSuccess(supertest, createdId); + await waitForRuleSuccessOrStatus(supertest, createdId); await waitForSignalsToBePresent(supertest, 1, [createdId]); // Get our single signal on top of a signal @@ -660,7 +661,7 @@ export default ({ getService }: FtrProviderContext) => { const executeRuleAndGetSignals = async (rule: QueryCreateSchema) => { const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsResponse = await getSignalsByIds(supertest, [id]); const signals = signalsResponse.hits.hits.map((hit) => hit._source); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts index 87e3b145ed6fd..ee787f1b616e3 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts @@ -23,7 +23,7 @@ import { createRule, waitForSignalsToBePresent, getSignalsByIds, - waitForRuleSuccess, + waitForRuleSuccessOrStatus, getRuleForSignalTesting, } from '../../utils'; import { createUserAndRole } from '../roles_users_utils'; @@ -82,7 +82,7 @@ export default ({ getService }: FtrProviderContext) => { it('should be able to execute and get 10 signals', async () => { const rule = getRuleForSignalTesting(['auditbeat-*']); const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 10, [id]); const signalsOpen = await getSignalsByIds(supertest, [id]); expect(signalsOpen.hits.hits.length).equal(10); @@ -91,7 +91,7 @@ export default ({ getService }: FtrProviderContext) => { it('should be have set the signals in an open state initially', async () => { const rule = getRuleForSignalTesting(['auditbeat-*']); const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 10, [id]); const signalsOpen = await getSignalsByIds(supertest, [id]); const everySignalOpen = signalsOpen.hits.hits.every( @@ -107,7 +107,7 @@ export default ({ getService }: FtrProviderContext) => { it('should be able to get a count of 10 closed signals when closing 10', async () => { const rule = getRuleForSignalTesting(['auditbeat-*']); const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 10, [id]); const signalsOpen = await getSignalsByIds(supertest, [id]); const signalIds = signalsOpen.hits.hits.map((signal) => signal._id); @@ -134,7 +134,7 @@ export default ({ getService }: FtrProviderContext) => { it('should be able close signals immediately and they all should be closed', async () => { const rule = getRuleForSignalTesting(['auditbeat-*']); const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsByIds(supertest, [id]); const signalIds = signalsOpen.hits.hits.map((signal) => signal._id); @@ -169,7 +169,7 @@ export default ({ getService }: FtrProviderContext) => { it('should NOT be able to close signals with t1 analyst user', async () => { const rule = getRuleForSignalTesting(['auditbeat-*']); const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); await createUserAndRole(securityService, ROLES.t1_analyst); const signalsOpen = await getSignalsByIds(supertest, [id]); @@ -207,7 +207,7 @@ export default ({ getService }: FtrProviderContext) => { it('should be able to close signals with soc_manager user', async () => { const rule = getRuleForSignalTesting(['auditbeat-*']); const { id } = await createRule(supertest, rule); - await waitForRuleSuccess(supertest, id); + await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const userAndRole = ROLES.soc_manager; await createUserAndRole(securityService, userAndRole); diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 5a36b950b6a5b..9cff40758bd49 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -119,6 +119,25 @@ export const getRuleForSignalTesting = ( from: '1900-01-01T00:00:00.000Z', }); +export const getRuleForSignalTestingWithTimestampOverride = ( + index: string[], + ruleId = 'rule-1', + enabled = true, + timestampOverride = 'event.ingested' +): QueryCreateSchema => ({ + name: 'Signal Testing Query', + description: 'Tests a simple query', + enabled, + risk_score: 1, + rule_id: ruleId, + severity: 'high', + index, + type: 'query', + query: '*:*', + timestamp_override: timestampOverride, + from: '1900-01-01T00:00:00.000Z', +}); + /** * This is a typical simple rule for testing that is easy for most basic testing * @param ruleId The rule id @@ -864,21 +883,22 @@ export const getRule = async ( }; /** - * Waits for the rule in find status to be succeeded before continuing + * Waits for the rule in find status to be 'succeeded' + * or the provided status, before continuing * @param supertest Deps */ -export const waitForRuleSuccess = async ( +export const waitForRuleSuccessOrStatus = async ( supertest: SuperTest, - id: string + id: string, + status: 'succeeded' | 'failed' | 'partial failure' = 'succeeded' ): Promise => { - // wait for Task Manager to finish executing the rule await waitFor(async () => { const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) .set('kbn-xsrf', 'true') .send({ ids: [id] }) .expect(200); - return body[id]?.current_status?.status === 'succeeded'; + return body[id]?.current_status?.status === status; }, 'waitForRuleSuccess'); }; diff --git a/x-pack/test/functional/es_archives/security_solution/timestamp_override/data.json.gz b/x-pack/test/functional/es_archives/security_solution/timestamp_override/data.json.gz new file mode 100644 index 0000000000000..be351495c2f2e Binary files /dev/null and b/x-pack/test/functional/es_archives/security_solution/timestamp_override/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/security_solution/timestamp_override/mappings.json b/x-pack/test/functional/es_archives/security_solution/timestamp_override/mappings.json new file mode 100644 index 0000000000000..28de7eeb2eb01 --- /dev/null +++ b/x-pack/test/functional/es_archives/security_solution/timestamp_override/mappings.json @@ -0,0 +1,19 @@ +{ + "type": "index", + "value": { + "index": "myfakeindex-1", + "mappings" : { + "properties" : { + "message" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + } + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/services/ml/data_visualizer_index_based.ts b/x-pack/test/functional/services/ml/data_visualizer_index_based.ts index 5fc5caf81c23b..fc9dd3d7b033a 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_index_based.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_index_based.ts @@ -33,25 +33,33 @@ export function MachineLearningDataVisualizerIndexBasedProvider({ }, async assertTotalDocCountHeaderExist() { - await testSubjects.existOrFail(`mlDataVisualizerTotalDocCountHeader`); + await retry.tryForTime(5000, async () => { + await testSubjects.existOrFail(`mlDataVisualizerTotalDocCountHeader`); + }); }, async assertTotalDocCountChartExist() { - await testSubjects.existOrFail(`mlFieldDataDocumentCountChart`); + await retry.tryForTime(5000, async () => { + await testSubjects.existOrFail(`mlFieldDataDocumentCountChart`); + }); }, async assertFieldCountPanelExist() { - await testSubjects.existOrFail(`mlDataVisualizerFieldCountPanel`); + await retry.tryForTime(5000, async () => { + await testSubjects.existOrFail(`mlDataVisualizerFieldCountPanel`); + }); }, async assertMetricFieldsSummaryExist() { - await testSubjects.existOrFail(`mlDataVisualizerMetricFieldsSummary`); + await retry.tryForTime(5000, async () => { + await testSubjects.existOrFail(`mlDataVisualizerMetricFieldsSummary`); + }); }, async assertVisibleMetricFieldsCount(count: number) { const expectedCount = count.toString(); - await testSubjects.existOrFail('mlDataVisualizerVisibleMetricFieldsCount'); await retry.tryForTime(5000, async () => { + await testSubjects.existOrFail('mlDataVisualizerVisibleMetricFieldsCount'); const actualCount = await testSubjects.getVisibleText( 'mlDataVisualizerVisibleMetricFieldsCount' ); @@ -64,8 +72,8 @@ export function MachineLearningDataVisualizerIndexBasedProvider({ async assertTotalMetricFieldsCount(count: number) { const expectedCount = count.toString(); - await testSubjects.existOrFail('mlDataVisualizerMetricFieldsCount'); await retry.tryForTime(5000, async () => { + await testSubjects.existOrFail('mlDataVisualizerMetricFieldsCount'); const actualCount = await testSubjects.getVisibleText( 'mlDataVisualizerVisibleMetricFieldsCount' ); @@ -78,8 +86,8 @@ export function MachineLearningDataVisualizerIndexBasedProvider({ async assertVisibleFieldsCount(count: number) { const expectedCount = count.toString(); - await testSubjects.existOrFail('mlDataVisualizerVisibleFieldsCount'); await retry.tryForTime(5000, async () => { + await testSubjects.existOrFail('mlDataVisualizerVisibleFieldsCount'); const actualCount = await testSubjects.getVisibleText('mlDataVisualizerVisibleFieldsCount'); expect(expectedCount).to.eql( expectedCount, @@ -90,8 +98,8 @@ export function MachineLearningDataVisualizerIndexBasedProvider({ async assertTotalFieldsCount(count: number) { const expectedCount = count.toString(); - await testSubjects.existOrFail('mlDataVisualizerTotalFieldsCount'); await retry.tryForTime(5000, async () => { + await testSubjects.existOrFail('mlDataVisualizerTotalFieldsCount'); const actualCount = await testSubjects.getVisibleText('mlDataVisualizerTotalFieldsCount'); expect(expectedCount).to.contain( expectedCount, @@ -101,11 +109,15 @@ export function MachineLearningDataVisualizerIndexBasedProvider({ }, async assertFieldsSummaryExist() { - await testSubjects.existOrFail(`mlDataVisualizerFieldsSummary`); + await retry.tryForTime(5000, async () => { + await testSubjects.existOrFail(`mlDataVisualizerFieldsSummary`); + }); }, async assertDataVisualizerTableExist() { - await testSubjects.existOrFail(`mlDataVisualizerTable`); + await retry.tryForTime(5000, async () => { + await testSubjects.existOrFail(`mlDataVisualizerTable`); + }); }, async assertActionsPanelExists() { diff --git a/yarn.lock b/yarn.lock index de9efb26c384b..956630bafa935 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1439,10 +1439,10 @@ resolved "https://registry.yarnpkg.com/@elastic/eslint-plugin-eui/-/eslint-plugin-eui-0.0.2.tgz#56b9ef03984a05cc213772ae3713ea8ef47b0314" integrity sha512-IoxURM5zraoQ7C8f+mJb9HYSENiZGgRVcG4tLQxE61yHNNRDXtGDWTZh8N1KIHcsqN1CEPETjuzBXkJYF/fDiQ== -"@elastic/eui@30.6.0": - version "30.6.0" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-30.6.0.tgz#6653223223f52407ac05303825d9bd08382df1d5" - integrity sha512-40Jiy54MpJAx3lD3NSZZLkMkVySwKpX6RxIKnvT3somE95pwIjXrWB688m2nL2g05y7kNhjrhwfdctVzNXZENA== +"@elastic/eui@31.0.0": + version "31.0.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-31.0.0.tgz#7d17386c04a0ad343d70c3652902fcd3f46ed337" + integrity sha512-oj63HpQQKg/Cgwz5B0ZBQCkcgZiEdQzBT9PbmEiR/VRz5P0WqJpgZPyIF7jiFaFlGP1a9hPjkUTo+ramWNCpiw== dependencies: "@types/chroma-js" "^2.0.0" "@types/lodash" "^4.14.160" @@ -28340,20 +28340,20 @@ vega-event-selector@^2.0.6, vega-event-selector@~2.0.6: resolved "https://registry.yarnpkg.com/vega-event-selector/-/vega-event-selector-2.0.6.tgz#6beb00e066b78371dde1a0f40cb5e0bbaecfd8bc" integrity sha512-UwCu50Sqd8kNZ1X/XgiAY+QAyQUmGFAwyDu7y0T5fs6/TPQnDo/Bo346NgSgINBEhEKOAMY1Nd/rPOk4UEm/ew== -vega-expression@^3.0.0, vega-expression@~3.0.0: +vega-expression@^4.0.0, vega-expression@^4.0.1, vega-expression@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/vega-expression/-/vega-expression-4.0.1.tgz#c03e4fc68a00acac49557faa4e4ed6ac8a59c5fd" + integrity sha512-ZrDj0hP8NmrCpdLFf7Rd/xMUHGoSYsAOTaYp7uXZ2dkEH5x0uPy5laECMc8TiQvL8W+8IrN2HAWCMRthTSRe2Q== + dependencies: + vega-util "^1.16.0" + +vega-expression@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/vega-expression/-/vega-expression-3.0.0.tgz#39179d010b34c57513162bf1ab5a7bff4b31be91" integrity sha512-/ObjIOK94MB+ziTuh8HZt2eWlKUPT/piRJLal5tx5QL1sQbfRi++7lHKTaKMLXLqc4Xqp9/DewE3PqQ6tYzaUA== dependencies: vega-util "^1.15.2" -vega-expression@~3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/vega-expression/-/vega-expression-3.0.1.tgz#bbccd8f59371a537eab16f3d9eff5cbeaa27532d" - integrity sha512-+UwOFEkBnAWo8Zud6i8O4Pd2W6QqmPUOaAhjNtj0OxRL+d+Duoy7M4edUDZ+YuoUcMnjjBFfDQu7oRAA1fIMEQ== - dependencies: - vega-util "^1.15.2" - vega-force@~4.0.7: version "4.0.7" resolved "https://registry.yarnpkg.com/vega-force/-/vega-force-4.0.7.tgz#6dc39ecb7889d9102661244d62fbc8d8714162ee" @@ -28374,22 +28374,22 @@ vega-format@^1.0.4, vega-format@~1.0.4: vega-time "^2.0.3" vega-util "^1.15.2" -vega-functions@^5.8.0, vega-functions@~5.8.0: - version "5.8.0" - resolved "https://registry.yarnpkg.com/vega-functions/-/vega-functions-5.8.0.tgz#48e02b0e5b14261cd445bda3c4721a18b02c810c" - integrity sha512-xaUqWZHEX+EuJuKfN0Biux3rrCHDEHmMbW7LHYyyEqguR0i6+zhtOSUEWmYqDfzB/+BlIwCk5Vif6q6/mzJxbQ== +vega-functions@^5.10.0, vega-functions@~5.10.0: + version "5.10.0" + resolved "https://registry.yarnpkg.com/vega-functions/-/vega-functions-5.10.0.tgz#3d384111f13b3b0dd38a4fca656c5ae54b66e158" + integrity sha512-1l28OxUwOj8FEvRU62Oz2hiTuDECrvx1DPU1qLebBKhlgaKbcCk3XyHrn1kUzhMKpXq+SFv5VPxchZP47ASSvQ== dependencies: d3-array "^2.7.1" d3-color "^2.0.0" d3-geo "^2.0.1" vega-dataflow "^5.7.3" - vega-expression "^3.0.0" + vega-expression "^4.0.1" vega-scale "^7.1.1" vega-scenegraph "^4.9.2" - vega-selections "^5.1.4" + vega-selections "^5.1.5" vega-statistics "^1.7.9" vega-time "^2.0.4" - vega-util "^1.15.2" + vega-util "^1.16.0" vega-geo@~4.3.8: version "4.3.8" @@ -28453,14 +28453,14 @@ vega-loader@^4.3.2, vega-loader@^4.3.3, vega-loader@~4.4.0: vega-format "^1.0.4" vega-util "^1.16.0" -vega-parser@~6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/vega-parser/-/vega-parser-6.1.0.tgz#485fb6fcd79d14b09efee340e2b55fb510e57e20" - integrity sha512-u14bHXV8vtcuMIJkMNoDAJ4Xu3lwKIkep+YEkPumWvlwl3fClWy26EAcwTneeM3rXu2F6ZJI6W3ddu/If8u13w== +vega-parser@~6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/vega-parser/-/vega-parser-6.1.2.tgz#7f25751177e38c3239560a9c427ded8d2ba617bb" + integrity sha512-aGyZrNzPrBruEb/WhemKDuDjQsIkMDGIgnSJci0b+9ZVxjyAzMl7UfGbiYorPiJlnIercjUJbMoFD6fCIf4gqQ== dependencies: vega-dataflow "^5.7.3" vega-event-selector "^2.0.6" - vega-functions "^5.8.0" + vega-functions "^5.10.0" vega-scale "^7.1.1" vega-util "^1.15.2" @@ -28518,12 +28518,12 @@ vega-schema-url-parser@^2.1.0: resolved "https://registry.yarnpkg.com/vega-schema-url-parser/-/vega-schema-url-parser-2.1.0.tgz#847f9cf9f1624f36f8a51abc1adb41ebc6673cb4" integrity sha512-JHT1PfOyVzOohj89uNunLPirs05Nf59isPT5gnwIkJph96rRgTIBJE7l7yLqndd7fLjr3P8JXHGAryRp74sCaQ== -vega-selections@^5.1.4: - version "5.1.4" - resolved "https://registry.yarnpkg.com/vega-selections/-/vega-selections-5.1.4.tgz#cc086fac5b4e646f9f1e000777f8786782d8516a" - integrity sha512-L7CHwcIjVf90GoW2tS2x5O496O5Joaerp5A1KM6VJ1uo4z6KfqxY6M/328a/uaAs0LC5qbQgXT3htFbtUrPW/A== +vega-selections@^5.1.5: + version "5.1.5" + resolved "https://registry.yarnpkg.com/vega-selections/-/vega-selections-5.1.5.tgz#c7662edf26c1cfb18623573b30590c9774348d1c" + integrity sha512-oRSsfkqYqA5xfEJqDpgnSDd+w0k6p6SGYisMD6rGXMxuPl0x0Uy6RvDr4nbEtB+dpWdoWEvgrsZVS6axyDNWvQ== dependencies: - vega-expression "^3.0.0" + vega-expression "^4.0.0" vega-util "^1.15.2" vega-spec-injector@^0.0.2: @@ -28586,16 +28586,16 @@ vega-view-transforms@~4.5.8: vega-scenegraph "^4.9.2" vega-util "^1.15.2" -vega-view@~5.9.0: - version "5.9.0" - resolved "https://registry.yarnpkg.com/vega-view/-/vega-view-5.9.0.tgz#ee6d5abd66d2503dec71e05e7ca8cf813465ae3f" - integrity sha512-HqRFuqO2OwoPHHK+CVt8vB8fu2L8GjQerLpmEpglWtCPDns5+gn5B6F7M8Ah8v24WlfqW7cLrY81t9OloPZOyw== +vega-view@~5.9.2: + version "5.9.2" + resolved "https://registry.yarnpkg.com/vega-view/-/vega-view-5.9.2.tgz#cb957e481a952abbe7b3a11aa2d58cc728f295e7" + integrity sha512-XAwKWyVjLClR3aCbTLCWdZj7aZozOULNg7078GxJIgVcBJOENCAidceI/H7JieyUZ96p3AiEHLQdWr167InBpg== dependencies: d3-array "^2.7.1" d3-timer "^2.0.0" vega-dataflow "^5.7.3" vega-format "^1.0.4" - vega-functions "^5.8.0" + vega-functions "^5.10.0" vega-runtime "^6.1.3" vega-scenegraph "^4.9.2" vega-util "^1.15.2" @@ -28620,24 +28620,24 @@ vega-wordcloud@~4.1.3: vega-statistics "^1.7.9" vega-util "^1.15.2" -vega@^5.17.1: - version "5.17.1" - resolved "https://registry.yarnpkg.com/vega/-/vega-5.17.1.tgz#ac95144b40137201b9d71a13615cc5b6eac6e5f7" - integrity sha512-ev1S6ohnsyeqps/bUVbhByoAbucap8vXPuiAJcxxft/EpgQGbIX/x42l0ijc3U1QHow2Lr3khtE1RshyU4lW2w== +vega@^5.17.3: + version "5.17.3" + resolved "https://registry.yarnpkg.com/vega/-/vega-5.17.3.tgz#9901f24c8cf5ff2e98f3fddb372b8f5a6d8502d8" + integrity sha512-c8N2pNg9MMmC6shNpoxVw3aVp2XPFOgmWNX5BEOAdCaGHRnSgzNy44+gYdGRaIe6+ljTzZg99Mf+OLO50IP42A== dependencies: vega-crossfilter "~4.0.5" vega-dataflow "~5.7.3" vega-encode "~4.8.3" vega-event-selector "~2.0.6" - vega-expression "~3.0.1" + vega-expression "~4.0.1" vega-force "~4.0.7" vega-format "~1.0.4" - vega-functions "~5.8.0" + vega-functions "~5.10.0" vega-geo "~4.3.8" vega-hierarchy "~4.0.9" vega-label "~1.0.0" vega-loader "~4.4.0" - vega-parser "~6.1.0" + vega-parser "~6.1.2" vega-projection "~1.4.5" vega-regression "~1.0.9" vega-runtime "~6.1.3" @@ -28648,7 +28648,7 @@ vega@^5.17.1: vega-transforms "~4.9.3" vega-typings "~0.19.2" vega-util "~1.16.0" - vega-view "~5.9.0" + vega-view "~5.9.2" vega-view-transforms "~4.5.8" vega-voronoi "~4.1.5" vega-wordcloud "~4.1.3"