From bc0d9e70791be8dfdeec9770625950f884fb50e8 Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Mon, 7 Mar 2022 08:13:52 -0500 Subject: [PATCH 01/19] [Presentation] Fix some bugs with services dependency injection (#126936) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../create/dependency_manager.test.ts | 54 +++++++++++++------ .../services/create/dependency_manager.ts | 50 +++++++++++------ 2 files changed, 71 insertions(+), 33 deletions(-) diff --git a/src/plugins/presentation_util/public/services/create/dependency_manager.test.ts b/src/plugins/presentation_util/public/services/create/dependency_manager.test.ts index 29702c33568655..8e67dee3f8b6b4 100644 --- a/src/plugins/presentation_util/public/services/create/dependency_manager.test.ts +++ b/src/plugins/presentation_util/public/services/create/dependency_manager.test.ts @@ -24,6 +24,16 @@ describe('DependencyManager', () => { expect(DependencyManager.orderDependencies(graph)).toEqual(sortedTopology); }); + it('should include final vertex if it has dependencies', () => { + const graph = { + A: [], + B: [], + C: ['A', 'B'], + }; + const sortedTopology = ['A', 'B', 'C']; + expect(DependencyManager.orderDependencies(graph)).toEqual(sortedTopology); + }); + it('orderDependencies. Should return base topology if no depended vertices', () => { const graph = { N: [], @@ -34,22 +44,34 @@ describe('DependencyManager', () => { expect(DependencyManager.orderDependencies(graph)).toEqual(sortedTopology); }); - it('orderDependencies. Should detect circular dependencies and throw error with path', () => { - const graph = { - N: ['R'], - R: ['A'], - A: ['B'], - B: ['C'], - C: ['D'], - D: ['E'], - E: ['F'], - F: ['L'], - L: ['G'], - G: ['N'], - }; - const circularPath = ['N', 'R', 'A', 'B', 'C', 'D', 'E', 'F', 'L', 'G', 'N'].join(' -> '); - const errorMessage = `Circular dependency detected while setting up services: ${circularPath}`; + describe('circular dependencies', () => { + it('should detect circular dependencies and throw error with path', () => { + const graph = { + N: ['R'], + R: ['A'], + A: ['B'], + B: ['C'], + C: ['D'], + D: ['E'], + E: ['F'], + F: ['L'], + L: ['G'], + G: ['N'], + }; + const circularPath = ['G', 'L', 'F', 'E', 'D', 'C', 'B', 'A', 'R', 'N'].join(' -> '); + const errorMessage = `Circular dependency detected while setting up services: ${circularPath}`; + + expect(() => DependencyManager.orderDependencies(graph)).toThrowError(errorMessage); + }); + + it('should detect circular dependency if circular reference is the first dependency for a vertex', () => { + const graph = { + A: ['B'], + B: ['A', 'C'], + C: [], + }; - expect(() => DependencyManager.orderDependencies(graph)).toThrowError(errorMessage); + expect(() => DependencyManager.orderDependencies(graph)).toThrow(); + }); }); }); diff --git a/src/plugins/presentation_util/public/services/create/dependency_manager.ts b/src/plugins/presentation_util/public/services/create/dependency_manager.ts index de30b180607fe4..3925f3e9d9c4fe 100644 --- a/src/plugins/presentation_util/public/services/create/dependency_manager.ts +++ b/src/plugins/presentation_util/public/services/create/dependency_manager.ts @@ -41,7 +41,14 @@ export class DependencyManager { return cycleInfo; } - return DependencyManager.sortVerticesFrom(srcVertex, graph, sortedVertices, {}, {}); + return DependencyManager.sortVerticesFrom( + srcVertex, + graph, + sortedVertices, + {}, + {}, + cycleInfo + ); }, DependencyManager.createCycleInfo()); } @@ -58,24 +65,30 @@ export class DependencyManager { graph: Graph, sortedVertices: Set, visited: BreadCrumbs = {}, - inpath: BreadCrumbs = {} + inpath: BreadCrumbs = {}, + cycle: CycleDetectionResult ): CycleDetectionResult { visited[srcVertex] = true; inpath[srcVertex] = true; - const cycleInfo = graph[srcVertex]?.reduce | undefined>( - (info, vertex) => { - if (inpath[vertex]) { - const path = (Object.keys(inpath) as T[]).filter( - (visitedVertex) => inpath[visitedVertex] - ); - return DependencyManager.createCycleInfo([...path, vertex], true); - } else if (!visited[vertex]) { - return DependencyManager.sortVerticesFrom(vertex, graph, sortedVertices, visited, inpath); - } - return info; - }, - undefined - ); + + const vertexEdges = + graph[srcVertex] === undefined || graph[srcVertex] === null ? [] : graph[srcVertex]; + + cycle = vertexEdges!.reduce>((info, vertex) => { + if (inpath[vertex]) { + return { ...info, hasCycle: true }; + } else if (!visited[vertex]) { + return DependencyManager.sortVerticesFrom( + vertex, + graph, + sortedVertices, + visited, + inpath, + info + ); + } + return info; + }, cycle); inpath[srcVertex] = false; @@ -83,7 +96,10 @@ export class DependencyManager { sortedVertices.add(srcVertex); } - return cycleInfo ?? DependencyManager.createCycleInfo([...sortedVertices]); + return { + ...cycle, + path: [...sortedVertices], + }; } private static createCycleInfo( From f12891ee460536a9c7dbd1848290f0cb0d429502 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Mon, 7 Mar 2022 14:14:13 +0100 Subject: [PATCH 02/19] :bug: Handle case of undefined fitting for line/area (#126891) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/vis_types/xy/public/config/get_config.ts | 4 ++-- .../options/point_series/elastic_charts_options.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/vis_types/xy/public/config/get_config.ts b/src/plugins/vis_types/xy/public/config/get_config.ts index d7cf22625e10ed..7aad30c5b743e6 100644 --- a/src/plugins/vis_types/xy/public/config/get_config.ts +++ b/src/plugins/vis_types/xy/public/config/get_config.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { ScaleContinuousType } from '@elastic/charts'; +import { Fit, ScaleContinuousType } from '@elastic/charts'; import { Datatable } from '../../../../expressions/public'; import { BUCKET_TYPES } from '../../../../data/public'; @@ -92,7 +92,7 @@ export function getConfig( return { // NOTE: downscale ratio to match current vislib implementation markSizeRatio: radiusRatio * 0.6, - fittingFunction, + fittingFunction: fittingFunction ?? Fit.Linear, fillOpacity, detailedTooltip, orderBucketsBySum, diff --git a/src/plugins/vis_types/xy/public/editor/components/options/point_series/elastic_charts_options.tsx b/src/plugins/vis_types/xy/public/editor/components/options/point_series/elastic_charts_options.tsx index 105cd667990416..1c93fe92b79af7 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/point_series/elastic_charts_options.tsx +++ b/src/plugins/vis_types/xy/public/editor/components/options/point_series/elastic_charts_options.tsx @@ -78,7 +78,7 @@ export function ElasticChartsOptions(props: ValidationVisOptionsProps })} options={fittingFunctions} paramName="fittingFunction" - value={stateParams.fittingFunction} + value={stateParams.fittingFunction ?? fittingFunctions[2].value} setValue={(paramName, value) => { if (trackUiMetric) { trackUiMetric(METRIC_TYPE.CLICK, 'fitting_function_selected'); From 0807b53d75f663db4fb8508e4aa729aaba2ad792 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Mon, 7 Mar 2022 07:23:35 -0600 Subject: [PATCH 03/19] fix data view load err msg (#126974) --- src/plugins/data_views/common/data_views/data_views.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/data_views/common/data_views/data_views.ts b/src/plugins/data_views/common/data_views/data_views.ts index 2e31ed793c3dbb..04c1fd98a0f608 100644 --- a/src/plugins/data_views/common/data_views/data_views.ts +++ b/src/plugins/data_views/common/data_views/data_views.ts @@ -424,7 +424,7 @@ export class DataViewsService { ); if (!savedObject.version) { - throw new SavedObjectNotFound(DATA_VIEW_SAVED_OBJECT_TYPE, id, 'management/kibana/dataViews'); + throw new SavedObjectNotFound('data view', id, 'management/kibana/dataViews'); } return this.initFromSavedObject(savedObject); From 0adb328a9af39d35c02442198a74476ba86d9687 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Mon, 7 Mar 2022 07:24:14 -0600 Subject: [PATCH 04/19] [data views] Reenable data view validation functional test (#125892) * reenable test --- ...e_delete.js => _index_pattern_create_delete.ts} | 14 ++++++++------ test/functional/page_objects/settings_page.ts | 13 +++++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) rename test/functional/apps/management/{_index_pattern_create_delete.js => _index_pattern_create_delete.ts} (91%) diff --git a/test/functional/apps/management/_index_pattern_create_delete.js b/test/functional/apps/management/_index_pattern_create_delete.ts similarity index 91% rename from test/functional/apps/management/_index_pattern_create_delete.js rename to test/functional/apps/management/_index_pattern_create_delete.ts index 4c9f5a5210ac68..6b2036499a1edd 100644 --- a/test/functional/apps/management/_index_pattern_create_delete.js +++ b/test/functional/apps/management/_index_pattern_create_delete.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const browser = getService('browser'); @@ -35,8 +36,7 @@ export default function ({ getService, getPageObjects }) { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/124663 - describe.skip('validation', function () { + describe('validation', function () { it('can display errors', async function () { await PageObjects.settings.clickAddNewIndexPatternButton(); await PageObjects.settings.setIndexPatternField('log-fake*'); @@ -46,7 +46,7 @@ export default function ({ getService, getPageObjects }) { it('can resolve errors and submit', async function () { await PageObjects.settings.setIndexPatternField('log*'); - await (await PageObjects.settings.getSaveIndexPatternButton()).click(); + await (await PageObjects.settings.getSaveDataViewButtonActive()).click(); await PageObjects.settings.removeIndexPattern(); }); }); @@ -72,10 +72,12 @@ export default function ({ getService, getPageObjects }) { }); describe('index pattern creation', function indexPatternCreation() { - let indexPatternId; + let indexPatternId: string; before(function () { - return PageObjects.settings.createIndexPattern().then((id) => (indexPatternId = id)); + return PageObjects.settings + .createIndexPattern('logstash-*') + .then((id) => (indexPatternId = id)); }); it('should have index pattern in page header', async function () { diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 70cdbea7fa8970..9c0fc73a23675e 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -164,6 +164,19 @@ export class SettingsPageObject extends FtrService { return await this.testSubjects.find('saveIndexPatternButton'); } + async getSaveDataViewButtonActive() { + await this.retry.try(async () => { + expect( + ( + await this.find.allByCssSelector( + '[data-test-subj="saveIndexPatternButton"]:not(.euiButton-isDisabled)' + ) + ).length + ).to.be(1); + }); + return await this.testSubjects.find('saveIndexPatternButton'); + } + async getCreateButton() { return await this.find.displayedByCssSelector('[type="submit"]'); } From 8b82657d46e18920e3a1acc3133f10f78e25a6f4 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Mon, 7 Mar 2022 07:24:39 -0600 Subject: [PATCH 05/19] [data views] functional tests to typescript (#126977) js => ts --- ...ard.js => _create_index_pattern_wizard.ts} | 6 ++-- ...x_pattern.js => _exclude_index_pattern.ts} | 3 +- .../{_handle_alias.js => _handle_alias.ts} | 3 +- ...onflict.js => _handle_version_conflict.ts} | 4 +-- ...ern_filter.js => _index_pattern_filter.ts} | 5 +-- ...larity.js => _index_pattern_popularity.ts} | 5 +-- ...sort.js => _index_pattern_results_sort.ts} | 10 +++--- ...kibana_settings.js => _kibana_settings.ts} | 3 +- ...jects.js => _mgmt_import_saved_objects.ts} | 9 ++--- ...{_runtime_fields.js => _runtime_fields.ts} | 9 +++-- ...scripted_fields.js => _scripted_fields.ts} | 35 ++++++++++--------- ...s_filter.js => _scripted_fields_filter.ts} | 3 +- ...preview.js => _scripted_fields_preview.ts} | 9 ++--- ...st_huge_fields.js => _test_huge_fields.ts} | 5 +-- test/functional/page_objects/settings_page.ts | 6 ++-- 15 files changed, 65 insertions(+), 50 deletions(-) rename test/functional/apps/management/{_create_index_pattern_wizard.js => _create_index_pattern_wizard.ts} (93%) rename test/functional/apps/management/{_exclude_index_pattern.js => _exclude_index_pattern.ts} (89%) rename test/functional/apps/management/{_handle_alias.js => _handle_alias.ts} (95%) rename test/functional/apps/management/{_handle_version_conflict.js => _handle_version_conflict.ts} (96%) rename test/functional/apps/management/{_index_pattern_filter.js => _index_pattern_filter.ts} (90%) rename test/functional/apps/management/{_index_pattern_popularity.js => _index_pattern_popularity.ts} (92%) rename test/functional/apps/management/{_index_pattern_results_sort.js => _index_pattern_results_sort.ts} (90%) rename test/functional/apps/management/{_kibana_settings.js => _kibana_settings.ts} (96%) rename test/functional/apps/management/{_mgmt_import_saved_objects.js => _mgmt_import_saved_objects.ts} (80%) rename test/functional/apps/management/{_runtime_fields.js => _runtime_fields.ts} (91%) rename test/functional/apps/management/{_scripted_fields.js => _scripted_fields.ts} (96%) rename test/functional/apps/management/{_scripted_fields_filter.js => _scripted_fields_filter.ts} (95%) rename test/functional/apps/management/{_scripted_fields_preview.js => _scripted_fields_preview.ts} (90%) rename test/functional/apps/management/{_test_huge_fields.js => _test_huge_fields.ts} (90%) diff --git a/test/functional/apps/management/_create_index_pattern_wizard.js b/test/functional/apps/management/_create_index_pattern_wizard.ts similarity index 93% rename from test/functional/apps/management/_create_index_pattern_wizard.js rename to test/functional/apps/management/_create_index_pattern_wizard.ts index b2f24e530cb120..cf732e178aa74c 100644 --- a/test/functional/apps/management/_create_index_pattern_wizard.js +++ b/test/functional/apps/management/_create_index_pattern_wizard.ts @@ -6,7 +6,9 @@ * Side Public License, v 1. */ -export default function ({ getService, getPageObjects }) { +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); const es = getService('es'); @@ -38,7 +40,7 @@ export default function ({ getService, getPageObjects }) { body: { actions: [{ add: { index: 'blogs', alias: 'alias1' } }] }, }); - await PageObjects.settings.createIndexPattern('alias1', false); + await PageObjects.settings.createIndexPattern('alias1', null); }); it('can delete an index pattern', async () => { diff --git a/test/functional/apps/management/_exclude_index_pattern.js b/test/functional/apps/management/_exclude_index_pattern.ts similarity index 89% rename from test/functional/apps/management/_exclude_index_pattern.js rename to test/functional/apps/management/_exclude_index_pattern.ts index b71222c1ec44d6..8c20acdc21f926 100644 --- a/test/functional/apps/management/_exclude_index_pattern.js +++ b/test/functional/apps/management/_exclude_index_pattern.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['settings']); const es = getService('es'); diff --git a/test/functional/apps/management/_handle_alias.js b/test/functional/apps/management/_handle_alias.ts similarity index 95% rename from test/functional/apps/management/_handle_alias.js rename to test/functional/apps/management/_handle_alias.ts index 891e59d84a04bc..04496bf9ed7583 100644 --- a/test/functional/apps/management/_handle_alias.js +++ b/test/functional/apps/management/_handle_alias.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const es = getService('es'); const retry = getService('retry'); diff --git a/test/functional/apps/management/_handle_version_conflict.js b/test/functional/apps/management/_handle_version_conflict.ts similarity index 96% rename from test/functional/apps/management/_handle_version_conflict.js rename to test/functional/apps/management/_handle_version_conflict.ts index a04c5d34b2d351..2f65f966c55967 100644 --- a/test/functional/apps/management/_handle_version_conflict.js +++ b/test/functional/apps/management/_handle_version_conflict.ts @@ -16,8 +16,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const kibanaServer = getService('kibanaServer'); const browser = getService('browser'); @@ -93,7 +94,6 @@ export default function ({ getService, getPageObjects }) { expect(response.body.result).to.be('updated'); await PageObjects.settings.controlChangeSave(); await retry.try(async function () { - //await PageObjects.common.sleep(2000); const message = await PageObjects.common.closeToast(); expect(message).to.contain('Unable'); }); diff --git a/test/functional/apps/management/_index_pattern_filter.js b/test/functional/apps/management/_index_pattern_filter.ts similarity index 90% rename from test/functional/apps/management/_index_pattern_filter.js rename to test/functional/apps/management/_index_pattern_filter.ts index 3e9d316b59c618..afa64c474d39d4 100644 --- a/test/functional/apps/management/_index_pattern_filter.js +++ b/test/functional/apps/management/_index_pattern_filter.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const retry = getService('retry'); const PageObjects = getPageObjects(['settings']); @@ -23,7 +24,7 @@ export default function ({ getService, getPageObjects }) { }); beforeEach(async function () { - await PageObjects.settings.createIndexPattern(); + await PageObjects.settings.createIndexPattern('logstash-*'); }); afterEach(async function () { diff --git a/test/functional/apps/management/_index_pattern_popularity.js b/test/functional/apps/management/_index_pattern_popularity.ts similarity index 92% rename from test/functional/apps/management/_index_pattern_popularity.js rename to test/functional/apps/management/_index_pattern_popularity.ts index 1a71e4c5fbc68b..bff6cdce0f7a69 100644 --- a/test/functional/apps/management/_index_pattern_popularity.js +++ b/test/functional/apps/management/_index_pattern_popularity.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); const log = getService('log'); @@ -23,7 +24,7 @@ export default function ({ getService, getPageObjects }) { }); beforeEach(async () => { - await PageObjects.settings.createIndexPattern(); + await PageObjects.settings.createIndexPattern('logstash-*'); // increase Popularity of geo.coordinates log.debug('Starting openControlsByName (' + fieldName + ')'); await PageObjects.settings.openControlsByName(fieldName); diff --git a/test/functional/apps/management/_index_pattern_results_sort.js b/test/functional/apps/management/_index_pattern_results_sort.ts similarity index 90% rename from test/functional/apps/management/_index_pattern_results_sort.js rename to test/functional/apps/management/_index_pattern_results_sort.ts index cedf5ee355b36a..305a72889e95ab 100644 --- a/test/functional/apps/management/_index_pattern_results_sort.js +++ b/test/functional/apps/management/_index_pattern_results_sort.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const retry = getService('retry'); const PageObjects = getPageObjects(['settings', 'common']); @@ -18,7 +19,7 @@ export default function ({ getService, getPageObjects }) { // delete .kibana index and then wait for Kibana to re-create it await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); - await PageObjects.settings.createIndexPattern(); + await PageObjects.settings.createIndexPattern('logstash-*'); }); after(async function () { @@ -30,7 +31,7 @@ export default function ({ getService, getPageObjects }) { heading: 'Name', first: '@message', last: 'xss.raw', - selector: async function () { + async selector() { const tableRow = await PageObjects.settings.getTableRow(0, 0); return await tableRow.getVisibleText(); }, @@ -39,7 +40,7 @@ export default function ({ getService, getPageObjects }) { heading: 'Type', first: '', last: 'text', - selector: async function () { + async selector() { const tableRow = await PageObjects.settings.getTableRow(0, 1); return await tableRow.getVisibleText(); }, @@ -49,7 +50,6 @@ export default function ({ getService, getPageObjects }) { columns.forEach(function (col) { describe('sort by heading - ' + col.heading, function indexPatternCreation() { it('should sort ascending', async function () { - console.log('col.heading', col.heading); if (col.heading !== 'Name') { await PageObjects.settings.sortBy(col.heading); } diff --git a/test/functional/apps/management/_kibana_settings.js b/test/functional/apps/management/_kibana_settings.ts similarity index 96% rename from test/functional/apps/management/_kibana_settings.js rename to test/functional/apps/management/_kibana_settings.ts index cfe4e88cda21de..d459643849fbc0 100644 --- a/test/functional/apps/management/_kibana_settings.js +++ b/test/functional/apps/management/_kibana_settings.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const browser = getService('browser'); const PageObjects = getPageObjects(['settings', 'common', 'dashboard', 'timePicker', 'header']); diff --git a/test/functional/apps/management/_mgmt_import_saved_objects.js b/test/functional/apps/management/_mgmt_import_saved_objects.ts similarity index 80% rename from test/functional/apps/management/_mgmt_import_saved_objects.js rename to test/functional/apps/management/_mgmt_import_saved_objects.ts index 95b0bbb7ed03b3..04a1bb59383223 100644 --- a/test/functional/apps/management/_mgmt_import_saved_objects.js +++ b/test/functional/apps/management/_mgmt_import_saved_objects.ts @@ -8,13 +8,14 @@ import expect from '@kbn/expect'; import path from 'path'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects']); - //in 6.4.0 bug the Saved Search conflict would be resolved and get imported but the visualization - //that referenced the saved search was not imported.( https://github.com/elastic/kibana/issues/22238) + // in 6.4.0 bug the Saved Search conflict would be resolved and get imported but the visualization + // that referenced the saved search was not imported.( https://github.com/elastic/kibana/issues/22238) describe('mgmt saved objects', function describeIndexTests() { before(async () => { @@ -41,7 +42,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.savedObjects.waitTableIsLoaded(); await PageObjects.savedObjects.searchForObject('mysaved'); - //instead of asserting on count- am asserting on the titles- which is more accurate than count. + // instead of asserting on count- am asserting on the titles- which is more accurate than count. const objects = await PageObjects.savedObjects.getRowTitles(); expect(objects.includes('mysavedsearch')).to.be(true); expect(objects.includes('mysavedviz')).to.be(true); diff --git a/test/functional/apps/management/_runtime_fields.js b/test/functional/apps/management/_runtime_fields.ts similarity index 91% rename from test/functional/apps/management/_runtime_fields.js rename to test/functional/apps/management/_runtime_fields.ts index 3a70df81b55d96..8ec9fb92c58eae 100644 --- a/test/functional/apps/management/_runtime_fields.js +++ b/test/functional/apps/management/_runtime_fields.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const log = getService('log'); const browser = getService('browser'); @@ -36,7 +37,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickIndexPatternLogstash(); - const startingCount = parseInt(await PageObjects.settings.getFieldsTabCount()); + const startingCount = parseInt(await PageObjects.settings.getFieldsTabCount(), 10); await log.debug('add runtime field'); await PageObjects.settings.addRuntimeField( fieldName, @@ -51,7 +52,9 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.clickSaveField(); await retry.try(async function () { - expect(parseInt(await PageObjects.settings.getFieldsTabCount())).to.be(startingCount + 1); + expect(parseInt(await PageObjects.settings.getFieldsTabCount(), 10)).to.be( + startingCount + 1 + ); }); }); diff --git a/test/functional/apps/management/_scripted_fields.js b/test/functional/apps/management/_scripted_fields.ts similarity index 96% rename from test/functional/apps/management/_scripted_fields.js rename to test/functional/apps/management/_scripted_fields.ts index 72f45e1fedb4db..c8c605ec7ed19e 100644 --- a/test/functional/apps/management/_scripted_fields.js +++ b/test/functional/apps/management/_scripted_fields.ts @@ -23,8 +23,9 @@ // it will automatically insert a a closing square brace ], etc. import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const log = getService('log'); const browser = getService('browser'); @@ -77,7 +78,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickIndexPatternLogstash(); - const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount()); + const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10); await PageObjects.settings.clickScriptedFieldsTab(); await log.debug('add scripted field'); const script = `1`; @@ -90,7 +91,7 @@ export default function ({ getService, getPageObjects }) { script ); await retry.try(async function () { - expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount())).to.be( + expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10)).to.be( startingCount + 1 ); }); @@ -111,7 +112,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickIndexPatternLogstash(); - const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount()); + const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10); await PageObjects.settings.clickScriptedFieldsTab(); await log.debug('add scripted field'); const script = `if (doc['machine.ram'].size() == 0) return -1; @@ -126,7 +127,7 @@ export default function ({ getService, getPageObjects }) { script ); await retry.try(async function () { - expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount())).to.be( + expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10)).to.be( startingCount + 1 ); }); @@ -150,7 +151,7 @@ export default function ({ getService, getPageObjects }) { }); }); - //add a test to sort numeric scripted field + // add a test to sort numeric scripted field it('should sort scripted field value in Discover', async function () { await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName}`); // after the first click on the scripted field, it becomes secondary sort after time. @@ -201,7 +202,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickIndexPatternLogstash(); - const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount()); + const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10); await PageObjects.settings.clickScriptedFieldsTab(); await log.debug('add scripted field'); await PageObjects.settings.addScriptedField( @@ -213,7 +214,7 @@ export default function ({ getService, getPageObjects }) { "if (doc['response.raw'].value == '200') { return 'good'} else { return 'bad'}" ); await retry.try(async function () { - expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount())).to.be( + expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10)).to.be( startingCount + 1 ); }); @@ -237,7 +238,7 @@ export default function ({ getService, getPageObjects }) { }); }); - //add a test to sort string scripted field + // add a test to sort string scripted field it('should sort scripted field value in Discover', async function () { await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); // after the first click on the scripted field, it becomes secondary sort after time. @@ -287,7 +288,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickIndexPatternLogstash(); - const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount()); + const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10); await PageObjects.settings.clickScriptedFieldsTab(); await log.debug('add scripted field'); await PageObjects.settings.addScriptedField( @@ -299,7 +300,7 @@ export default function ({ getService, getPageObjects }) { "doc['response.raw'].value == '200'" ); await retry.try(async function () { - expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount())).to.be( + expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10)).to.be( startingCount + 1 ); }); @@ -335,8 +336,8 @@ export default function ({ getService, getPageObjects }) { await filterBar.removeAllFilters(); }); - //add a test to sort boolean - //existing bug: https://github.com/elastic/kibana/issues/75519 hence the issue is skipped. + // add a test to sort boolean + // existing bug: https://github.com/elastic/kibana/issues/75519 hence the issue is skipped. it.skip('should sort scripted field value in Discover', async function () { await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); // after the first click on the scripted field, it becomes secondary sort after time. @@ -374,7 +375,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickIndexPatternLogstash(); - const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount()); + const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10); await PageObjects.settings.clickScriptedFieldsTab(); await log.debug('add scripted field'); await PageObjects.settings.addScriptedField( @@ -386,7 +387,7 @@ export default function ({ getService, getPageObjects }) { "doc['utc_time'].value.toEpochMilli() + (1000) * 60 * 60" ); await retry.try(async function () { - expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount())).to.be( + expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10)).to.be( startingCount + 1 ); }); @@ -410,8 +411,8 @@ export default function ({ getService, getPageObjects }) { }); }); - //add a test to sort date scripted field - //https://github.com/elastic/kibana/issues/75711 + // add a test to sort date scripted field + // https://github.com/elastic/kibana/issues/75711 it.skip('should sort scripted field value in Discover', async function () { await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); // after the first click on the scripted field, it becomes secondary sort after time. diff --git a/test/functional/apps/management/_scripted_fields_filter.js b/test/functional/apps/management/_scripted_fields_filter.ts similarity index 95% rename from test/functional/apps/management/_scripted_fields_filter.js rename to test/functional/apps/management/_scripted_fields_filter.ts index abae9a300994dc..82d15908197506 100644 --- a/test/functional/apps/management/_scripted_fields_filter.js +++ b/test/functional/apps/management/_scripted_fields_filter.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const retry = getService('retry'); const log = getService('log'); diff --git a/test/functional/apps/management/_scripted_fields_preview.js b/test/functional/apps/management/_scripted_fields_preview.ts similarity index 90% rename from test/functional/apps/management/_scripted_fields_preview.js rename to test/functional/apps/management/_scripted_fields_preview.ts index b6c941fe21d0ac..380b4659c0f38f 100644 --- a/test/functional/apps/management/_scripted_fields_preview.js +++ b/test/functional/apps/management/_scripted_fields_preview.ts @@ -7,13 +7,14 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const PageObjects = getPageObjects(['settings']); const SCRIPTED_FIELD_NAME = 'myScriptedField'; - const scriptResultToJson = (scriptResult) => { + const scriptResultToJson = (scriptResult: string) => { try { return JSON.parse(scriptResult); } catch (e) { @@ -26,7 +27,7 @@ export default function ({ getService, getPageObjects }) { await browser.setWindowSize(1200, 800); await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); - await PageObjects.settings.createIndexPattern(); + await PageObjects.settings.createIndexPattern('logstash-*'); await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); @@ -67,7 +68,7 @@ export default function ({ getService, getPageObjects }) { it('should display additional fields', async function () { const scriptResults = await PageObjects.settings.executeScriptedField( `doc['bytes'].value * 2`, - ['bytes'] + 'bytes' ); const [{ _id, bytes }] = scriptResultToJson(scriptResults); expect(_id).to.be.a('string'); diff --git a/test/functional/apps/management/_test_huge_fields.js b/test/functional/apps/management/_test_huge_fields.ts similarity index 90% rename from test/functional/apps/management/_test_huge_fields.js rename to test/functional/apps/management/_test_huge_fields.ts index 7b756839409286..abc338cb8abc82 100644 --- a/test/functional/apps/management/_test_huge_fields.js +++ b/test/functional/apps/management/_test_huge_fields.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const security = getService('security'); const PageObjects = getPageObjects(['common', 'home', 'settings']); @@ -19,7 +20,7 @@ export default function ({ getService, getPageObjects }) { const EXPECTED_FIELD_COUNT = '10006'; before(async function () { - await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader'], false); + await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader']); await esArchiver.emptyKibanaIndex(); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/large_fields'); await PageObjects.settings.navigateTo(); diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 9c0fc73a23675e..98fdff82e13c55 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -563,7 +563,7 @@ export class SettingsPageObject extends FtrService { name: string, language: string, type: string, - format: Record, + format: Record | null, popularity: string, script: string ) { @@ -803,7 +803,7 @@ export class SettingsPageObject extends FtrService { await this.flyout.ensureClosed('scriptedFieldsHelpFlyout'); } - async executeScriptedField(script: string, additionalField: string) { + async executeScriptedField(script: string, additionalField?: string) { this.log.debug('execute Scripted Fields help'); await this.closeScriptedFieldHelp(); // ensure script help is closed so script input is not blocked await this.setScriptedFieldScript(script); @@ -814,7 +814,7 @@ export class SettingsPageObject extends FtrService { await this.testSubjects.click('runScriptButton'); await this.testSubjects.waitForDeleted('.euiLoadingSpinner'); } - let scriptResults; + let scriptResults: string = ''; await this.retry.try(async () => { scriptResults = await this.testSubjects.getVisibleText('scriptedFieldPreview'); }); From d0b64d9cafb7ae33bb53f6f5437e3394a7210f33 Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Mon, 7 Mar 2022 13:55:24 +0000 Subject: [PATCH 06/19] [ML] Add API tests for analytics jobs_exist and new_job_caps endpoints (#126914) * [ML] Add API tests for analytics jobs_exist and new_job_caps endpoints * [ML] Edits following review * [ML] Edit to api doc Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../ml/server/routes/data_frame_analytics.ts | 10 +- .../apis/ml/data_frame_analytics/index.ts | 2 + .../data_frame_analytics/jobs_exist_spaces.ts | 97 ++++++++++++++++ .../ml/data_frame_analytics/new_job_caps.ts | 104 ++++++++++++++++++ 4 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 x-pack/test/api_integration/apis/ml/data_frame_analytics/jobs_exist_spaces.ts create mode 100644 x-pack/test/api_integration/apis/ml/data_frame_analytics/new_job_caps.ts diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index 2ab10bda361909..1fa7217e7d252d 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -609,12 +609,12 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense, routeGuard }: Rout /** * @apiGroup DataFrameAnalytics * - * @api {post} /api/ml/data_frame/analytics/job_exists Check whether jobs exists in current or any space - * @apiName JobExists - * @apiDescription Checks if each of the jobs in the specified list of IDs exist. + * @api {post} /api/ml/data_frame/analytics/jobs_exist Check whether jobs exist in current or any space + * @apiName JobsExist + * @apiDescription Checks if each of the jobs in the specified list of IDs exists. * If allSpaces is true, the check will look across all spaces. * - * @apiSchema (params) analyticsIdSchema + * @apiSchema (params) jobsExistSchema */ router.post( { @@ -707,7 +707,7 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense, routeGuard }: Rout /** * @apiGroup DataFrameAnalytics * - * @api {get} api/data_frame/analytics/fields/:indexPattern Get fields for a pattern of indices used for analytics + * @api {get} /api/ml/data_frame/analytics/new_job_caps/:indexPattern Get fields for a pattern of indices used for analytics * @apiName AnalyticsNewJobCaps * @apiDescription Retrieve the index fields for analytics */ diff --git a/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts b/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts index 21ff8f2cc64c16..9c9bcb318e7ec2 100644 --- a/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts +++ b/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts @@ -22,5 +22,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./delete_spaces')); loadTestFile(require.resolve('./evaluate')); loadTestFile(require.resolve('./explain')); + loadTestFile(require.resolve('./jobs_exist_spaces')); + loadTestFile(require.resolve('./new_job_caps')); }); } diff --git a/x-pack/test/api_integration/apis/ml/data_frame_analytics/jobs_exist_spaces.ts b/x-pack/test/api_integration/apis/ml/data_frame_analytics/jobs_exist_spaces.ts new file mode 100644 index 00000000000000..4934af379ae660 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/data_frame_analytics/jobs_exist_spaces.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; +import { USER } from '../../../../functional/services/ml/security_common'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + const spacesService = getService('spaces'); + const supertest = getService('supertestWithoutAuth'); + + const jobIdSpace1 = 'ihp_od_space1'; + const jobIdSpace2 = 'ihp_od_space2'; + const idSpace1 = 'space1'; + const idSpace2 = 'space2'; + + const initialModelMemoryLimit = '17mb'; + + async function runRequest( + space: string, + expectedStatusCode: number, + analyticsIds?: string[], + allSpaces?: boolean + ) { + const { body } = await supertest + .post(`/s/${space}/api/ml/data_frame/analytics/jobs_exist`) + .auth( + USER.ML_VIEWER_ALL_SPACES, + ml.securityCommon.getPasswordForUser(USER.ML_VIEWER_ALL_SPACES) + ) + .set(COMMON_REQUEST_HEADERS) + .send(allSpaces ? { analyticsIds, allSpaces } : { analyticsIds }) + .expect(expectedStatusCode); + + return body; + } + + describe('POST data_frame/analytics/jobs_exist with spaces', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ihp_outlier'); + await spacesService.create({ id: idSpace1, name: 'space_one', disabledFeatures: [] }); + await spacesService.create({ id: idSpace2, name: 'space_two', disabledFeatures: [] }); + + const jobConfigSpace1 = ml.commonConfig.getDFAIhpOutlierDetectionJobConfig(jobIdSpace1); + await ml.api.createDataFrameAnalyticsJob( + { ...jobConfigSpace1, model_memory_limit: initialModelMemoryLimit }, + idSpace1 + ); + + const jobConfigSpace2 = ml.commonConfig.getDFAIhpOutlierDetectionJobConfig(jobIdSpace2); + await ml.api.createDataFrameAnalyticsJob( + { ...jobConfigSpace2, model_memory_limit: initialModelMemoryLimit }, + idSpace2 + ); + + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + after(async () => { + await spacesService.delete(idSpace1); + await spacesService.delete(idSpace2); + await ml.api.cleanMlIndices(); + await ml.testResources.cleanMLSavedObjects(); + }); + + it('should find single job from same space', async () => { + const body = await runRequest(idSpace1, 200, [jobIdSpace1]); + expect(body).to.eql({ [jobIdSpace1]: { exists: true } }); + }); + + it('should not find single job from different space', async () => { + const body = await runRequest(idSpace2, 200, [jobIdSpace1]); + expect(body).to.eql({ [jobIdSpace1]: { exists: false } }); + }); + + it('should only find job from same space when called with a list of jobs', async () => { + const body = await runRequest(idSpace1, 200, [jobIdSpace1, jobIdSpace2]); + expect(body).to.eql({ + [jobIdSpace1]: { exists: true }, + [jobIdSpace2]: { exists: false }, + }); + }); + + it('should find single job from different space when run across all spaces', async () => { + const body = await runRequest(idSpace1, 200, [jobIdSpace2], true); + expect(body).to.eql({ [jobIdSpace2]: { exists: true } }); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/data_frame_analytics/new_job_caps.ts b/x-pack/test/api_integration/apis/ml/data_frame_analytics/new_job_caps.ts new file mode 100644 index 00000000000000..72ac632a8b8dd2 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/data_frame_analytics/new_job_caps.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; +import { USER } from '../../../../functional/services/ml/security_common'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + const supertest = getService('supertestWithoutAuth'); + const testIndexPattern = 'ft_bank_marketing'; + + async function runRequest(indexPattern: string, expectedStatusCode: number, rollup?: boolean) { + let url = `/api/ml/data_frame/analytics/new_job_caps/${indexPattern}`; + if (rollup !== undefined) { + url += `?rollup=${rollup}`; + } + const { body } = await supertest + .get(url) + .auth( + USER.ML_VIEWER_ALL_SPACES, + ml.securityCommon.getPasswordForUser(USER.ML_VIEWER_ALL_SPACES) + ) + .set(COMMON_REQUEST_HEADERS) + .expect(expectedStatusCode); + + return body; + } + + describe('GET data_frame/analytics/new_job_caps', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/bm_classification'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should return job capabilities of fields for an index that exists', async () => { + const body = await runRequest(testIndexPattern, 200); + await ml.testExecution.logTestStep( + `response should contain object for ${testIndexPattern} index pattern` + ); + expect(body).to.have.keys(testIndexPattern); + const testIndexPatternCaps = body[testIndexPattern]; + + // The data frame analytics UI does not use the aggs prop, so just perform basic checks this prop + await ml.testExecution.logTestStep( + `should contain aggs and fields props for ${testIndexPattern} index pattern` + ); + expect(testIndexPatternCaps).to.have.keys('aggs', 'fields'); + const aggs = testIndexPatternCaps.aggs; + expect(aggs).to.have.length(35); + + // The data frames analytics UI uses this endpoint to extract the names and types of fields, + // so check this info is present for some example fields + const fields = testIndexPatternCaps.fields; + expect(fields).to.have.length(24); + + await ml.testExecution.logTestStep( + `fields should contain expected name and type attributes for ${testIndexPattern} index pattern` + ); + const balanceTextField = fields.find((obj: any) => obj.id === 'balance'); + expect(balanceTextField).to.have.keys('name', 'type'); + expect(balanceTextField.name).to.eql('balance'); + expect(balanceTextField.type).to.eql('text'); + + const balanceKeywordField = fields.find((obj: any) => obj.id === 'balance.keyword'); + expect(balanceKeywordField).to.have.keys('name', 'type'); + expect(balanceKeywordField.name).to.eql('balance.keyword'); + expect(balanceKeywordField.type).to.eql('keyword'); + }); + + it('should fail to return job capabilities of fields for an index that does not exist', async () => { + await runRequest(`${testIndexPattern}_invalid`, 404); + }); + + it('should return empty job capabilities of fields for a non-rollup index with rollup parameter set to true', async () => { + const body = await runRequest(testIndexPattern, 200, true); + await ml.testExecution.logTestStep( + `response should contain object for ${testIndexPattern} index pattern` + ); + expect(body).to.have.keys(testIndexPattern); + const testIndexPatternCaps = body[testIndexPattern]; + + await ml.testExecution.logTestStep( + `should contain empty aggs and fields props for ${testIndexPattern} index pattern` + ); + expect(testIndexPatternCaps).to.have.keys('aggs', 'fields'); + const aggs = testIndexPatternCaps.aggs; + expect(aggs).to.have.length(0); + const fields = testIndexPatternCaps.fields; + expect(fields).to.have.length(0); + }); + }); +}; From 3fe1270dd710ce81842356b254d78e9277aae007 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Mon, 7 Mar 2022 14:56:46 +0100 Subject: [PATCH 07/19] [Lens][Embeddable] Make Embeddable resilient when toggling actions (#126558) * :bug: Push to the bottom embeddable creation to better handle lifecycles * Update x-pack/plugins/lens/public/embeddable/embeddable_component.tsx * Update x-pack/plugins/lens/public/embeddable/embeddable_component.tsx * Update x-pack/plugins/lens/public/embeddable/embeddable_component.tsx Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../embeddable/embeddable_component.tsx | 53 ++++++++++++------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx b/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx index 482a5b931ed78a..f44aef76ab83d4 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx @@ -11,6 +11,7 @@ import type { Action, UiActionsStart } from 'src/plugins/ui_actions/public'; import type { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { EuiLoadingChart } from '@elastic/eui'; import { + EmbeddableFactory, EmbeddableInput, EmbeddableOutput, EmbeddablePanel, @@ -69,41 +70,48 @@ interface PluginsStartDependencies { } export function getEmbeddableComponent(core: CoreStart, plugins: PluginsStartDependencies) { + const { embeddable: embeddableStart, uiActions, inspector } = plugins; + const factory = embeddableStart.getEmbeddableFactory('lens')!; + const theme = core.theme; return (props: EmbeddableComponentProps) => { - const { embeddable: embeddableStart, uiActions, inspector } = plugins; - const factory = embeddableStart.getEmbeddableFactory('lens')!; const input = { ...props }; - const [embeddable, loading, error] = useEmbeddableFactory({ factory, input }); const hasActions = - Boolean(props.withDefaultActions) || (props.extraActions && props.extraActions?.length > 0); + Boolean(input.withDefaultActions) || (input.extraActions && input.extraActions?.length > 0); - const theme = core.theme; - - if (loading) { - return ; - } - - if (embeddable && hasActions) { + if (hasActions) { return ( } + factory={factory} uiActions={uiActions} inspector={inspector} actionPredicate={() => hasActions} input={input} theme={theme} - extraActions={props.extraActions} - withDefaultActions={props.withDefaultActions} + extraActions={input.extraActions} + withDefaultActions={input.withDefaultActions} /> ); } - - return ; + return ; }; } +function EmbeddableRootWrapper({ + factory, + input, +}: { + factory: EmbeddableFactory; + input: EmbeddableComponentProps; +}) { + const [embeddable, loading, error] = useEmbeddableFactory({ factory, input }); + if (loading) { + return ; + } + return ; +} + interface EmbeddablePanelWrapperProps { - embeddable: IEmbeddable; + factory: EmbeddableFactory; uiActions: PluginsStartDependencies['uiActions']; inspector: PluginsStartDependencies['inspector']; actionPredicate: (id: string) => boolean; @@ -114,7 +122,7 @@ interface EmbeddablePanelWrapperProps { } const EmbeddablePanelWrapper: FC = ({ - embeddable, + factory, uiActions, actionPredicate, inspector, @@ -123,10 +131,17 @@ const EmbeddablePanelWrapper: FC = ({ extraActions, withDefaultActions, }) => { + const [embeddable, loading] = useEmbeddableFactory({ factory, input }); useEffect(() => { - embeddable.updateInput(input); + if (embeddable) { + embeddable.updateInput(input); + } }, [embeddable, input]); + if (loading || !embeddable) { + return ; + } + return ( Date: Mon, 7 Mar 2022 15:58:24 +0200 Subject: [PATCH 08/19] [Cloud Posture] Update cloud security posture code owners (#127004) --- .github/CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 691daa042bba95..0a0aa994fb70bf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -425,8 +425,8 @@ x-pack/plugins/session_view @elastic/awp-platform # Security Asset Management /x-pack/plugins/osquery @elastic/security-asset-management -# Cloud Posture Security -/x-pack/plugins/cloud_security_posture/ @elastic/cloud-posture-security +# Cloud Security Posture +/x-pack/plugins/cloud_security_posture/ @elastic/cloud-security-posture-control-plane # Design (at the bottom for specificity of SASS files) **/*.scss @elastic/kibana-design From d5a1cdc1781bac6633e21d04b8598302dda3d158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=BCrkan=20YALAMAN?= Date: Mon, 7 Mar 2022 15:03:10 +0100 Subject: [PATCH 09/19] [App Search] Add an audit modal to show last changes on a modal. (#126486) * Add a last change column to the engine overview table * Add an audit modal to show last changes on the modal. --- .../enterprise_search/common/constants.ts | 1 + .../audit_logs_modal/audit_logs_modal.scss | 9 ++ .../audit_logs_modal.test.tsx | 52 ++++++++ .../audit_logs_modal/audit_logs_modal.tsx | 121 ++++++++++++++++++ .../audit_logs_modal_logic.test.ts | 53 ++++++++ .../audit_logs_modal_logic.ts | 32 +++++ .../components/tables/engine_link_helpers.tsx | 9 ++ .../components/tables/engines_table.tsx | 17 ++- .../components/tables/meta_engines_table.tsx | 14 ++ .../components/tables/shared_columns.tsx | 11 ++ .../components/engines/engines_overview.tsx | 2 + .../enterprise_search/server/plugin.ts | 9 ++ 12 files changed, 328 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal_logic.ts diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts index 7a6203c994f4d9..456a76d914f7d2 100644 --- a/x-pack/plugins/enterprise_search/common/constants.ts +++ b/x-pack/plugins/enterprise_search/common/constants.ts @@ -89,3 +89,4 @@ export const READ_ONLY_MODE_HEADER = 'x-ent-search-read-only-mode'; export const ENTERPRISE_SEARCH_KIBANA_COOKIE = '_enterprise_search'; export const LOGS_SOURCE_ID = 'ent-search-logs'; +export const ENTERPRISE_SEARCH_AUDIT_LOGS_SOURCE_ID = 'ent-search-audit-logs'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.scss new file mode 100644 index 00000000000000..11a008a3cc51fc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.scss @@ -0,0 +1,9 @@ +.auditLogsModal { + width: 75vw; +} + +@media (max-width: 1200px) { + .auditLogsModal { + width: 100vw; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.test.tsx new file mode 100644 index 00000000000000..f6687e431e9836 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.test.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LogicMounter, setMockValues, setMockActions } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiText, EuiModal } from '@elastic/eui'; + +import { EntSearchLogStream } from '../../../../../shared/log_stream'; + +import { AuditLogsModal } from './audit_logs_modal'; + +import { AuditLogsModalLogic } from './audit_logs_modal_logic'; + +describe('AuditLogsModal', () => { + const { mount } = new LogicMounter(AuditLogsModalLogic); + beforeEach(() => { + jest.clearAllMocks(); + mount({ isModalVisible: true }); + }); + + it('renders nothing by default', () => { + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('renders the modal when modal visible', () => { + const testEngineName = 'test-engine-123'; + const mockClose = jest.fn(); + setMockValues({ + isModalVisible: true, + engineName: testEngineName, + }); + setMockActions({ + hideModal: mockClose, + }); + + const wrapper = shallow(); + expect(wrapper.find(EntSearchLogStream).prop('query')).toBe( + `event.kind: event and event.action: audit and enterprisesearch.data_repository.name: ${testEngineName}` + ); + expect(wrapper.find(EuiText).children().text()).toBe('Showing events from last 24 hours'); + expect(wrapper.find(EuiModal).prop('onClose')).toBe(mockClose); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.tsx new file mode 100644 index 00000000000000..3807234fd5c118 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.tsx @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues, useActions } from 'kea'; + +import { + EuiButton, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { ENTERPRISE_SEARCH_AUDIT_LOGS_SOURCE_ID } from '../../../../../../../common/constants'; +import { EntSearchLogStream } from '../../../../../shared/log_stream'; + +import { AuditLogsModalLogic } from './audit_logs_modal_logic'; + +import './audit_logs_modal.scss'; + +export const AuditLogsModal: React.FC = () => { + const auditLogsModalLogic = AuditLogsModalLogic(); + const { isModalVisible, engineName } = useValues(auditLogsModalLogic); + const { hideModal } = useActions(auditLogsModalLogic); + + const filters = [ + 'event.kind: event', + 'event.action: audit', + `enterprisesearch.data_repository.name: ${engineName}`, + ].join(' and '); + + return !isModalVisible ? null : ( + + + +

{engineName}

+
+
+ + + {i18n.translate('xpack.enterpriseSearch.appSearch.engines.auditLogsModal.eventTip', { + defaultMessage: 'Showing events from last 24 hours', + })} + + + + + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engines.auditLogsModal.closeButton', { + defaultMessage: 'Close', + })} + + +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal_logic.test.ts new file mode 100644 index 00000000000000..f869dd145087d3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal_logic.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LogicMounter } from '../../../../../__mocks__/kea_logic'; + +import { AuditLogsModalLogic } from './audit_logs_modal_logic'; + +describe('AuditLogsModalLogic', () => { + const { mount } = new LogicMounter(AuditLogsModalLogic); + + beforeEach(() => { + jest.clearAllMocks(); + mount(); + }); + + it('has excepted default values', () => { + expect(AuditLogsModalLogic.values).toEqual({ + isModalVisible: false, + engineName: '', + }); + }); + + describe('actions', () => { + describe('hideModal', () => { + it('hides the modal', () => { + mount({ + isModalVisible: true, + engineName: 'test_engine', + }); + + AuditLogsModalLogic.actions.hideModal(); + expect(AuditLogsModalLogic.values).toEqual({ + isModalVisible: false, + engineName: '', + }); + }); + }); + + describe('showModal', () => { + it('show the modal with correct engine name', () => { + AuditLogsModalLogic.actions.showModal('test-engine-123'); + expect(AuditLogsModalLogic.values).toEqual({ + isModalVisible: true, + engineName: 'test-engine-123', + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal_logic.ts new file mode 100644 index 00000000000000..afa70b4f3dee0e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal_logic.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea } from 'kea'; + +export const AuditLogsModalLogic = kea({ + path: ['enterprise_search', 'app_search', 'engines_overview', 'audit_logs_modal'], + actions: () => ({ + hideModal: true, + showModal: (engineName: string) => ({ engineName }), + }), + reducers: () => ({ + isModalVisible: [ + false, + { + showModal: () => true, + hideModal: () => false, + }, + ], + engineName: [ + '', + { + showModal: (_, { engineName }) => engineName, + hideModal: () => '', + }, + ], + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/engine_link_helpers.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/engine_link_helpers.tsx index a3350d1ef9939c..229e0def4700e9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/engine_link_helpers.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/engine_link_helpers.tsx @@ -7,11 +7,14 @@ import React from 'react'; +import { EuiLink } from '@elastic/eui'; + import { KibanaLogic } from '../../../../../shared/kibana'; import { EuiLinkTo } from '../../../../../shared/react_router_helpers'; import { TelemetryLogic } from '../../../../../shared/telemetry'; import { ENGINE_PATH } from '../../../../routes'; import { generateEncodedPath } from '../../../../utils/encode_path_params'; +import { FormattedDateTime } from '../../../../utils/formatted_date_time'; const sendEngineTableLinkClickTelemetry = () => { TelemetryLogic.actions.sendAppSearchTelemetry({ @@ -34,3 +37,9 @@ export const renderEngineLink = (engineName: string) => ( {engineName} ); + +export const renderLastChangeLink = (dateString: string, onClick = () => {}) => ( + + {!dateString ? '-' : } + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/engines_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/engines_table.tsx index 563e272a4a7303..5e6ece1003e7fc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/engines_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/engines_table.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import { useValues } from 'kea'; +import { useActions, useValues } from 'kea'; import { EuiBasicTable, EuiBasicTableColumn, EuiTableFieldDataColumnType } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -16,10 +16,13 @@ import { AppLogic } from '../../../../app_logic'; import { UNIVERSAL_LANGUAGE } from '../../../../constants'; import { EngineDetails } from '../../../engine/types'; -import { renderEngineLink } from './engine_link_helpers'; +import { AuditLogsModalLogic } from '../audit_logs_modal/audit_logs_modal_logic'; + +import { renderEngineLink, renderLastChangeLink } from './engine_link_helpers'; import { ACTIONS_COLUMN, CREATED_AT_COLUMN, + LAST_UPDATED_COLUMN, DOCUMENT_COUNT_COLUMN, FIELD_COUNT_COLUMN, NAME_COLUMN, @@ -46,12 +49,22 @@ export const EnginesTable: React.FC = ({ myRole: { canManageEngines }, } = useValues(AppLogic); + const { showModal: showAuditLogModal } = useActions(AuditLogsModalLogic); + const columns: Array> = [ { ...NAME_COLUMN, render: (name: string) => renderEngineLink(name), }, CREATED_AT_COLUMN, + { + ...LAST_UPDATED_COLUMN, + render: (dateString: string, engineDetails) => { + return renderLastChangeLink(dateString, () => { + showAuditLogModal(engineDetails.name); + }); + }, + }, LANGUAGE_COLUMN, DOCUMENT_COUNT_COLUMN, FIELD_COUNT_COLUMN, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table.tsx index f99dc7e15eaec5..24eb8cc8a6b812 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table.tsx @@ -14,6 +14,9 @@ import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; import { AppLogic } from '../../../../app_logic'; import { EngineDetails } from '../../../engine/types'; +import { AuditLogsModalLogic } from '../audit_logs_modal/audit_logs_modal_logic'; + +import { renderLastChangeLink } from './engine_link_helpers'; import { MetaEnginesTableExpandedRow } from './meta_engines_table_expanded_row'; import { MetaEnginesTableLogic } from './meta_engines_table_logic'; import { MetaEnginesTableNameColumnContent } from './meta_engines_table_name_column_content'; @@ -21,6 +24,7 @@ import { ACTIONS_COLUMN, BLANK_COLUMN, CREATED_AT_COLUMN, + LAST_UPDATED_COLUMN, DOCUMENT_COUNT_COLUMN, FIELD_COUNT_COLUMN, NAME_COLUMN, @@ -49,6 +53,8 @@ export const MetaEnginesTable: React.FC = ({ myRole: { canManageMetaEngines }, } = useValues(AppLogic); + const { showModal: showAuditLogModal } = useActions(AuditLogsModalLogic); + const conflictingEnginesSets: ConflictingEnginesSets = useMemo( () => items.reduce((accumulator, metaEngine) => { @@ -89,6 +95,14 @@ export const MetaEnginesTable: React.FC = ({ ), }, CREATED_AT_COLUMN, + { + ...LAST_UPDATED_COLUMN, + render: (dateString: string, engineDetails) => { + return renderLastChangeLink(dateString, () => { + showAuditLogModal(engineDetails.name); + }); + }, + }, BLANK_COLUMN, DOCUMENT_COUNT_COLUMN, FIELD_COUNT_COLUMN, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/shared_columns.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/shared_columns.tsx index 325760b641efdc..b0ca36a7778389 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/shared_columns.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/shared_columns.tsx @@ -50,6 +50,17 @@ export const CREATED_AT_COLUMN: EuiTableFieldDataColumnType = { render: (dateString: string) => , }; +export const LAST_UPDATED_COLUMN: EuiTableFieldDataColumnType = { + field: 'updated_at', + name: i18n.translate( + 'xpack.enterpriseSearch.appSearch.enginesOverview.table.column.lastUpdated', + { + defaultMessage: 'Last updated', + } + ), + dataType: 'string', +}; + export const DOCUMENT_COUNT_COLUMN: EuiTableFieldDataColumnType = { field: 'document_count', name: i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx index f8df9f5abfaa54..27cdff5d69812e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx @@ -21,6 +21,7 @@ import { DataPanel } from '../data_panel'; import { AppSearchPageTemplate } from '../layout'; import { EmptyState, EmptyMetaEnginesState } from './components'; +import { AuditLogsModal } from './components/audit_logs_modal/audit_logs_modal'; import { EnginesTable } from './components/tables/engines_table'; import { MetaEnginesTable } from './components/tables/meta_engines_table'; import { @@ -144,6 +145,7 @@ export const EnginesOverview: React.FC = () => { data-test-subj="metaEnginesLicenseCTA" /> )} + ); }; diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index ef9a0cea9da60f..f393ca59a44118 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -27,6 +27,7 @@ import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN, LOGS_SOURCE_ID, + ENTERPRISE_SEARCH_AUDIT_LOGS_SOURCE_ID, } from '../common/constants'; import { registerTelemetryUsageCollector as registerASTelemetryUsageCollector } from './collectors/app_search/telemetry'; @@ -185,6 +186,14 @@ export class EnterpriseSearchPlugin implements Plugin { indexName: '.ent-search-*', }, }); + + infra.defineInternalSourceConfiguration(ENTERPRISE_SEARCH_AUDIT_LOGS_SOURCE_ID, { + name: 'Enterprise Search Audit Logs', + logIndices: { + type: 'index_name', + indexName: 'logs-enterprise_search*', + }, + }); } public start() {} From 90f0d8de0130ec0b00dd666df1f99bb3a62ef27a Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Mon, 7 Mar 2022 07:07:04 -0700 Subject: [PATCH 10/19] [Reporting] Use the logger from Core instead of a wrapper (#126740) * [Reporting] Use the logger from Core instead of a wrapper * fix redudant log context in execute fns Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/reporting/server/config/config.ts | 7 +- .../server/config/create_config.test.ts | 13 ++-- .../reporting/server/config/create_config.ts | 7 +- x-pack/plugins/reporting/server/core.ts | 9 +-- .../common/decrypt_job_headers.test.ts | 4 +- .../common/decrypt_job_headers.ts | 5 +- .../export_types/common/generate_png.ts | 8 +-- .../common/get_custom_logo.test.ts | 11 ++-- .../export_types/common/get_custom_logo.ts | 5 +- .../csv_searchsource/execute_job.test.ts | 11 ++-- .../csv_searchsource/execute_job.ts | 3 +- .../generate_csv/generate_csv.test.ts | 17 ++--- .../generate_csv/generate_csv.ts | 15 ++--- .../generate_csv/get_export_settings.test.ts | 12 ++-- .../generate_csv/get_export_settings.ts | 5 +- .../csv_searchsource_immediate/execute_job.ts | 5 +- .../png/execute_job/index.test.ts | 14 ++-- .../export_types/png/execute_job/index.ts | 4 +- .../export_types/png_v2/execute_job.test.ts | 12 +--- .../server/export_types/png_v2/execute_job.ts | 4 +- .../printable_pdf/execute_job/index.test.ts | 12 +--- .../printable_pdf/execute_job/index.ts | 4 +- .../printable_pdf/lib/generate_pdf.ts | 6 +- .../printable_pdf_v2/execute_job.test.ts | 12 +--- .../printable_pdf_v2/execute_job.ts | 4 +- .../printable_pdf_v2/lib/generate_pdf.ts | 12 ++-- .../server/lib/check_params_version.ts | 6 +- .../server/lib/content_stream.test.ts | 8 +-- .../reporting/server/lib/content_stream.ts | 15 ++--- .../server/lib/event_logger/adapter.test.ts | 8 +-- .../server/lib/event_logger/adapter.ts | 15 +++-- .../server/lib/event_logger/logger.test.ts | 4 +- .../server/lib/event_logger/logger.ts | 5 +- x-pack/plugins/reporting/server/lib/index.ts | 1 - .../reporting/server/lib/level_logger.ts | 65 ------------------- .../reporting/server/lib/store/store.test.ts | 10 +-- .../reporting/server/lib/store/store.ts | 17 ++--- .../server/lib/tasks/error_logger.test.ts | 4 +- .../server/lib/tasks/error_logger.ts | 4 +- .../server/lib/tasks/execute_report.test.ts | 9 +-- .../server/lib/tasks/execute_report.ts | 19 +++--- .../server/lib/tasks/monitor_report.test.ts | 9 +-- .../server/lib/tasks/monitor_reports.ts | 11 ++-- .../plugins/reporting/server/plugin.test.ts | 12 ++-- x-pack/plugins/reporting/server/plugin.ts | 8 +-- .../routes/deprecations/deprecations.ts | 10 +-- .../integration_tests/deprecations.test.ts | 6 +- .../server/routes/diagnostic/browser.ts | 6 +- .../server/routes/diagnostic/index.ts | 4 +- .../integration_tests/browser.test.ts | 4 +- .../integration_tests/screenshot.test.ts | 4 +- .../server/routes/diagnostic/screenshot.ts | 2 +- .../generate/csv_searchsource_immediate.ts | 12 ++-- .../generate/generate_from_jobparams.ts | 8 +-- .../generation_from_jobparams.test.ts | 4 +- .../plugins/reporting/server/routes/index.ts | 4 +- .../reporting/server/routes/lib/jobs_query.ts | 4 +- .../server/routes/lib/request_handler.test.ts | 10 +-- .../server/routes/lib/request_handler.ts | 12 ++-- .../test_helpers/create_mock_levellogger.ts | 29 --------- .../create_mock_reportingplugin.ts | 12 ++-- .../reporting/server/test_helpers/index.ts | 1 - x-pack/plugins/reporting/server/types.ts | 7 +- 63 files changed, 223 insertions(+), 367 deletions(-) delete mode 100644 x-pack/plugins/reporting/server/lib/level_logger.ts delete mode 100644 x-pack/plugins/reporting/server/test_helpers/create_mock_levellogger.ts diff --git a/x-pack/plugins/reporting/server/config/config.ts b/x-pack/plugins/reporting/server/config/config.ts index 00c57053653f75..269a66503a741f 100644 --- a/x-pack/plugins/reporting/server/config/config.ts +++ b/x-pack/plugins/reporting/server/config/config.ts @@ -7,8 +7,7 @@ import { get } from 'lodash'; import { first } from 'rxjs/operators'; -import { CoreSetup, PluginInitializerContext } from 'src/core/server'; -import { LevelLogger } from '../lib'; +import type { CoreSetup, Logger, PluginInitializerContext } from 'kibana/server'; import { createConfig$ } from './create_config'; import { ReportingConfigType } from './schema'; @@ -63,13 +62,13 @@ export interface ReportingConfig extends Config { * @internal * @param {PluginInitializerContext} initContext * @param {CoreSetup} core - * @param {LevelLogger} logger + * @param {Logger} logger * @returns {Promise} */ export const buildConfig = async ( initContext: PluginInitializerContext, core: CoreSetup, - logger: LevelLogger + logger: Logger ): Promise => { const config$ = initContext.config.create(); const { http } = core; diff --git a/x-pack/plugins/reporting/server/config/create_config.test.ts b/x-pack/plugins/reporting/server/config/create_config.test.ts index fd8180bd46a05d..f839d72e1a45df 100644 --- a/x-pack/plugins/reporting/server/config/create_config.test.ts +++ b/x-pack/plugins/reporting/server/config/create_config.test.ts @@ -6,11 +6,10 @@ */ import * as Rx from 'rxjs'; -import { CoreSetup, HttpServerInfo, PluginInitializerContext } from 'src/core/server'; -import { coreMock } from 'src/core/server/mocks'; -import { LevelLogger } from '../lib/level_logger'; -import { createMockConfigSchema, createMockLevelLogger } from '../test_helpers'; -import { ReportingConfigType } from './'; +import type { CoreSetup, HttpServerInfo, Logger, PluginInitializerContext } from 'kibana/server'; +import { coreMock, loggingSystemMock } from 'src/core/server/mocks'; +import { createMockConfigSchema } from '../test_helpers'; +import type { ReportingConfigType } from './'; import { createConfig$ } from './create_config'; const createMockConfig = ( @@ -20,14 +19,14 @@ const createMockConfig = ( describe('Reporting server createConfig$', () => { let mockCoreSetup: CoreSetup; let mockInitContext: PluginInitializerContext; - let mockLogger: jest.Mocked; + let mockLogger: jest.Mocked; beforeEach(() => { mockCoreSetup = coreMock.createSetup(); mockInitContext = coreMock.createPluginInitializerContext( createMockConfigSchema({ kibanaServer: {} }) ); - mockLogger = createMockLevelLogger(); + mockLogger = loggingSystemMock.createLogger(); }); afterEach(() => { diff --git a/x-pack/plugins/reporting/server/config/create_config.ts b/x-pack/plugins/reporting/server/config/create_config.ts index 2ac225ec4576a8..ff8d00c30d4f83 100644 --- a/x-pack/plugins/reporting/server/config/create_config.ts +++ b/x-pack/plugins/reporting/server/config/create_config.ts @@ -7,11 +7,10 @@ import crypto from 'crypto'; import ipaddr from 'ipaddr.js'; +import type { CoreSetup, Logger } from 'kibana/server'; import { sum } from 'lodash'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { CoreSetup } from 'src/core/server'; -import { LevelLogger } from '../lib'; import { ReportingConfigType } from './schema'; /* @@ -22,9 +21,9 @@ import { ReportingConfigType } from './schema'; export function createConfig$( core: CoreSetup, config$: Observable, - parentLogger: LevelLogger + parentLogger: Logger ) { - const logger = parentLogger.clone(['config']); + const logger = parentLogger.get('config'); return config$.pipe( map((config) => { // encryption key diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index 745542c358a69b..a4e4f43f90e1ee 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -11,6 +11,7 @@ import { filter, first, map, switchMap, take } from 'rxjs/operators'; import type { BasePath, IClusterClient, + Logger, PackageInfo, PluginInitializerContext, SavedObjectsClientContract, @@ -32,7 +33,7 @@ import { REPORTING_REDIRECT_LOCATOR_STORE_KEY } from '../common/constants'; import { durationToNumber } from '../common/schema_utils'; import type { ReportingConfig, ReportingSetup } from './'; import { ReportingConfigType } from './config'; -import { checkLicense, getExportTypesRegistry, LevelLogger } from './lib'; +import { checkLicense, getExportTypesRegistry } from './lib'; import { reportingEventLoggerFactory } from './lib/event_logger/logger'; import type { IReport, ReportingStore } from './lib/store'; import { ExecuteReportTask, MonitorReportsTask, ReportTaskParams } from './lib/tasks'; @@ -45,7 +46,7 @@ export interface ReportingInternalSetup { security?: SecurityPluginSetup; spaces?: SpacesPluginSetup; taskManager: TaskManagerSetupContract; - logger: LevelLogger; + logger: Logger; status: StatusServiceSetup; } @@ -57,7 +58,7 @@ export interface ReportingInternalStart { data: DataPluginStart; fieldFormats: FieldFormatsStart; licensing: LicensingPluginStart; - logger: LevelLogger; + logger: Logger; screenshotting: ScreenshottingStart; security?: SecurityPluginStart; taskManager: TaskManagerStartContract; @@ -81,7 +82,7 @@ export class ReportingCore { public getContract: () => ReportingSetup; - constructor(private logger: LevelLogger, context: PluginInitializerContext) { + constructor(private logger: Logger, context: PluginInitializerContext) { this.packageInfo = context.env.packageInfo; const syncConfig = context.config.get(); this.deprecatedAllowedRoles = syncConfig.roles.enabled ? syncConfig.roles.allow : false; diff --git a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts index b5258d91485f70..56a1c39e75aa46 100644 --- a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts @@ -5,11 +5,11 @@ * 2.0. */ +import { loggingSystemMock } from 'src/core/server/mocks'; import { cryptoFactory } from '../../lib'; -import { createMockLevelLogger } from '../../test_helpers'; import { decryptJobHeaders } from './'; -const logger = createMockLevelLogger(); +const logger = loggingSystemMock.createLogger(); const encryptHeaders = async (encryptionKey: string, headers: Record) => { const crypto = cryptoFactory(encryptionKey); diff --git a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts index f126d1edbfce3a..3dfcfe362abd49 100644 --- a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts @@ -6,12 +6,13 @@ */ import { i18n } from '@kbn/i18n'; -import { cryptoFactory, LevelLogger } from '../../lib'; +import type { Logger } from 'kibana/server'; +import { cryptoFactory } from '../../lib'; export const decryptJobHeaders = async ( encryptionKey: string | undefined, headers: string, - logger: LevelLogger + logger: Logger ): Promise> => { try { if (typeof headers !== 'string') { diff --git a/x-pack/plugins/reporting/server/export_types/common/generate_png.ts b/x-pack/plugins/reporting/server/export_types/common/generate_png.ts index caa0b7fb91b3f2..272d1c287178ad 100644 --- a/x-pack/plugins/reporting/server/export_types/common/generate_png.ts +++ b/x-pack/plugins/reporting/server/export_types/common/generate_png.ts @@ -6,14 +6,14 @@ */ import apm from 'elastic-apm-node'; +import type { Logger } from 'kibana/server'; import * as Rx from 'rxjs'; import { finalize, map, tap } from 'rxjs/operators'; +import type { ReportingCore } from '../../'; import { LayoutTypes } from '../../../../screenshotting/common'; import { REPORTING_TRANSACTION_TYPE } from '../../../common/constants'; import type { PngMetrics } from '../../../common/types'; -import { ReportingCore } from '../../'; -import { ScreenshotOptions } from '../../types'; -import { LevelLogger } from '../../lib'; +import type { ScreenshotOptions } from '../../types'; interface PngResult { buffer: Buffer; @@ -23,7 +23,7 @@ interface PngResult { export function generatePngObservable( reporting: ReportingCore, - logger: LevelLogger, + logger: Logger, options: ScreenshotOptions ): Rx.Observable { const apmTrans = apm.startTransaction('generate-png', REPORTING_TRANSACTION_TYPE); diff --git a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts index f5675b50cfddd4..850d0ae507e126 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts @@ -5,17 +5,14 @@ * 2.0. */ -import { ReportingCore } from '../..'; -import { - createMockConfigSchema, - createMockLevelLogger, - createMockReportingCore, -} from '../../test_helpers'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { ReportingCore } from '../../'; +import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import { getCustomLogo } from './get_custom_logo'; let mockReportingPlugin: ReportingCore; -const logger = createMockLevelLogger(); +const logger = loggingSystemMock.createLogger(); beforeEach(async () => { mockReportingPlugin = await createMockReportingCore(createMockConfigSchema()); diff --git a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts index fcabd34a642c8c..10873155039885 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts @@ -5,16 +5,15 @@ * 2.0. */ -import type { Headers } from 'src/core/server'; +import type { Headers, Logger } from 'kibana/server'; import { ReportingCore } from '../../'; import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../common/constants'; -import { LevelLogger } from '../../lib'; export const getCustomLogo = async ( reporting: ReportingCore, headers: Headers, spaceId: string | undefined, - logger: LevelLogger + logger: Logger ) => { const fakeRequest = reporting.getFakeRequest({ headers }, spaceId, logger); const uiSettingsClient = await reporting.getUiSettingsClient(fakeRequest, logger); diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts index ee6d6daab88e06..5a8c4f1fd760c0 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts @@ -16,18 +16,15 @@ jest.mock('./generate_csv/generate_csv', () => ({ }, })); -import { Writable } from 'stream'; import nodeCrypto from '@elastic/node-crypto'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { Writable } from 'stream'; import { ReportingCore } from '../../'; import { CancellationToken } from '../../../common/cancellation_token'; -import { - createMockConfigSchema, - createMockLevelLogger, - createMockReportingCore, -} from '../../test_helpers'; +import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import { runTaskFnFactory } from './execute_job'; -const logger = createMockLevelLogger(); +const logger = loggingSystemMock.createLogger(); const encryptionKey = 'tetkey'; const headers = { sid: 'cooltestheaders' }; let encryptedHeaders: string; diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.ts index 97f0aa65e3d68e..8b5f0e5395827b 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { CSV_JOB_TYPE } from '../../../common/constants'; import { getFieldFormats } from '../../services'; import { RunTaskFn, RunTaskFnFactory } from '../../types'; import { decryptJobHeaders } from '../common'; @@ -19,7 +18,7 @@ export const runTaskFnFactory: RunTaskFnFactory> = ( const config = reporting.getConfig(); return async function runTask(jobId, job, cancellationToken, stream) { - const logger = parentLogger.clone([CSV_JOB_TYPE, 'execute-job', jobId]); + const logger = parentLogger.get(`execute-job:${jobId}`); const encryptionKey = config.get('encryptionKey'); const headers = await decryptJobHeaders(encryptionKey, job.headers, logger); diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts index c525cb7c0def2d..4755d153666e40 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts @@ -5,21 +5,22 @@ * 2.0. */ -import { Writable } from 'stream'; -import * as Rx from 'rxjs'; import { errors as esErrors } from '@elastic/elasticsearch'; +import type { IScopedClusterClient, IUiSettingsClient, SearchResponse } from 'kibana/server'; import { identity, range } from 'lodash'; -import { IScopedClusterClient, IUiSettingsClient, SearchResponse } from 'src/core/server'; +import * as Rx from 'rxjs'; import { elasticsearchServiceMock, + loggingSystemMock, savedObjectsClientMock, uiSettingsServiceMock, } from 'src/core/server/mocks'; import { ISearchStartSearchSource } from 'src/plugins/data/common'; -import { FieldFormatsRegistry } from 'src/plugins/field_formats/common'; import { searchSourceInstanceMock } from 'src/plugins/data/common/search/search_source/mocks'; import { IScopedSearchClient } from 'src/plugins/data/server'; import { dataPluginMock } from 'src/plugins/data/server/mocks'; +import { FieldFormatsRegistry } from 'src/plugins/field_formats/common'; +import { Writable } from 'stream'; import { ReportingConfig } from '../../../'; import { CancellationToken } from '../../../../common/cancellation_token'; import { @@ -28,11 +29,7 @@ import { UI_SETTINGS_DATEFORMAT_TZ, } from '../../../../common/constants'; import { UnknownError } from '../../../../common/errors'; -import { - createMockConfig, - createMockConfigSchema, - createMockLevelLogger, -} from '../../../test_helpers'; +import { createMockConfig, createMockConfigSchema } from '../../../test_helpers'; import { JobParamsCSV } from '../types'; import { CsvGenerator } from './generate_csv'; @@ -125,7 +122,7 @@ beforeEach(async () => { }); }); -const logger = createMockLevelLogger(); +const logger = loggingSystemMock.createLogger(); it('formats an empty search result to CSV content', async () => { const generateCsv = new CsvGenerator( diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts index 201484af9d7d0a..c913706f585624 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts @@ -5,9 +5,9 @@ * 2.0. */ -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { errors as esErrors } from '@elastic/elasticsearch'; -import type { IScopedClusterClient, IUiSettingsClient } from 'src/core/server'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { IScopedClusterClient, IUiSettingsClient, Logger } from 'kibana/server'; import type { IScopedSearchClient } from 'src/plugins/data/server'; import type { Datatable } from 'src/plugins/expressions/server'; import type { Writable } from 'stream'; @@ -32,16 +32,15 @@ import type { CancellationToken } from '../../../../common/cancellation_token'; import { CONTENT_TYPE_CSV } from '../../../../common/constants'; import { AuthenticationExpiredError, - UnknownError, ReportingError, + UnknownError, } from '../../../../common/errors'; import { byteSizeValueToNumber } from '../../../../common/schema_utils'; -import type { LevelLogger } from '../../../lib'; import type { TaskRunResult } from '../../../lib/tasks'; import type { JobParamsCSV } from '../types'; import { CsvExportSettings, getExportSettings } from './get_export_settings'; -import { MaxSizeStringBuilder } from './max_size_string_builder'; import { i18nTexts } from './i18n_texts'; +import { MaxSizeStringBuilder } from './max_size_string_builder'; interface Clients { es: IScopedClusterClient; @@ -65,7 +64,7 @@ export class CsvGenerator { private clients: Clients, private dependencies: Dependencies, private cancellationToken: CancellationToken, - private logger: LevelLogger, + private logger: Logger, private stream: Writable ) {} @@ -316,7 +315,7 @@ export class CsvGenerator { } if (!results) { - this.logger.warning(`Search results are undefined!`); + this.logger.warn(`Search results are undefined!`); break; } @@ -396,7 +395,7 @@ export class CsvGenerator { this.logger.debug(`Finished generating. Row count: ${this.csvRowCount}.`); if (!this.maxSizeReached && this.csvRowCount !== totalRecords) { - this.logger.warning( + this.logger.warn( `ES scroll returned fewer total hits than expected! ` + `Search result total hits: ${totalRecords}. Row count: ${this.csvRowCount}.` ); diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.test.ts index 2ae3e5e712d313..ef0f0062bf19b6 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.test.ts @@ -12,18 +12,18 @@ import { UI_SETTINGS_SEARCH_INCLUDE_FROZEN, } from '../../../../common/constants'; import { IUiSettingsClient } from 'kibana/server'; -import { savedObjectsClientMock, uiSettingsServiceMock } from 'src/core/server/mocks'; import { - createMockConfig, - createMockConfigSchema, - createMockLevelLogger, -} from '../../../test_helpers'; + loggingSystemMock, + savedObjectsClientMock, + uiSettingsServiceMock, +} from 'src/core/server/mocks'; +import { createMockConfig, createMockConfigSchema } from '../../../test_helpers'; import { getExportSettings } from './get_export_settings'; describe('getExportSettings', () => { let uiSettingsClient: IUiSettingsClient; const config = createMockConfig(createMockConfigSchema({})); - const logger = createMockLevelLogger(); + const logger = loggingSystemMock.createLogger(); beforeEach(() => { uiSettingsClient = uiSettingsServiceMock diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.ts index 5b69e33624c5c2..6a07e3184eb48f 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.ts @@ -6,7 +6,7 @@ */ import { ByteSizeValue } from '@kbn/config-schema'; -import { IUiSettingsClient } from 'kibana/server'; +import type { IUiSettingsClient, Logger } from 'kibana/server'; import { createEscapeValue } from '../../../../../../../src/plugins/data/common'; import { ReportingConfig } from '../../../'; import { @@ -16,7 +16,6 @@ import { UI_SETTINGS_DATEFORMAT_TZ, UI_SETTINGS_SEARCH_INCLUDE_FROZEN, } from '../../../../common/constants'; -import { LevelLogger } from '../../../lib'; export interface CsvExportSettings { timezone: string; @@ -37,7 +36,7 @@ export const getExportSettings = async ( client: IUiSettingsClient, config: ReportingConfig, timezone: string | undefined, - logger: LevelLogger + logger: Logger ): Promise => { let setTimezone: string; if (timezone) { diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts index 53e1f6ba3c95bf..50ae2ab10f6e75 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts @@ -8,7 +8,6 @@ import { KibanaRequest } from 'src/core/server'; import { Writable } from 'stream'; import { CancellationToken } from '../../../common/cancellation_token'; -import { CSV_SEARCHSOURCE_IMMEDIATE_TYPE } from '../../../common/constants'; import { TaskRunResult } from '../../lib/tasks'; import { getFieldFormats } from '../../services'; import { ReportingRequestHandlerContext, RunTaskFnFactory } from '../../types'; @@ -32,7 +31,7 @@ export const runTaskFnFactory: RunTaskFnFactory = function e parentLogger ) { const config = reporting.getConfig(); - const logger = parentLogger.clone([CSV_SEARCHSOURCE_IMMEDIATE_TYPE, 'execute-job']); + const logger = parentLogger.get('execute-job'); return async function runTask(_jobId, immediateJobParams, context, stream, req) { const job = { @@ -82,7 +81,7 @@ export const runTaskFnFactory: RunTaskFnFactory = function e const { warnings } = result; if (warnings) { warnings.forEach((warning) => { - logger.warning(warning); + logger.warn(warning); }); } diff --git a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts index 9069ec63a88250..bc37978372ba63 100644 --- a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts +++ b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { Writable } from 'stream'; import * as Rx from 'rxjs'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { Writable } from 'stream'; import { ReportingCore } from '../../../'; import { CancellationToken } from '../../../../common/cancellation_token'; -import { cryptoFactory, LevelLogger } from '../../../lib'; +import { cryptoFactory } from '../../../lib'; import { createMockConfig, createMockConfigSchema, @@ -29,14 +30,7 @@ const cancellationToken = { on: jest.fn(), } as unknown as CancellationToken; -const mockLoggerFactory = { - get: jest.fn().mockImplementation(() => ({ - error: jest.fn(), - debug: jest.fn(), - warn: jest.fn(), - })), -}; -const getMockLogger = () => new LevelLogger(mockLoggerFactory); +const getMockLogger = () => loggingSystemMock.createLogger(); const mockEncryptionKey = 'abcabcsecuresecret'; const encryptHeaders = async (headers: Record) => { diff --git a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts index 67d013740bedd3..52023e53b80b56 100644 --- a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts @@ -8,7 +8,7 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { finalize, map, mergeMap, takeUntil, tap } from 'rxjs/operators'; -import { PNG_JOB_TYPE, REPORTING_TRANSACTION_TYPE } from '../../../../common/constants'; +import { REPORTING_TRANSACTION_TYPE } from '../../../../common/constants'; import { TaskRunResult } from '../../../lib/tasks'; import { RunTaskFn, RunTaskFnFactory } from '../../../types'; import { decryptJobHeaders, getFullUrls, generatePngObservable } from '../../common'; @@ -24,7 +24,7 @@ export const runTaskFnFactory: RunTaskFnFactory> = const apmGetAssets = apmTrans?.startSpan('get-assets', 'setup'); let apmGeneratePng: { end: () => void } | null | undefined; - const jobLogger = parentLogger.clone([PNG_JOB_TYPE, 'execute', jobId]); + const jobLogger = parentLogger.get(`execute:${jobId}`); const process$: Rx.Observable = Rx.of(1).pipe( mergeMap(() => decryptJobHeaders(encryptionKey, job.headers, jobLogger)), mergeMap((headers) => { diff --git a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts index 1b1ad6878d78fb..1403873e8da4b8 100644 --- a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts @@ -6,11 +6,12 @@ */ import * as Rx from 'rxjs'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { Writable } from 'stream'; import { ReportingCore } from '../../'; import { CancellationToken } from '../../../common/cancellation_token'; import { LocatorParams } from '../../../common/types'; -import { cryptoFactory, LevelLogger } from '../../lib'; +import { cryptoFactory } from '../../lib'; import { createMockConfig, createMockConfigSchema, @@ -30,14 +31,7 @@ const cancellationToken = { on: jest.fn(), } as unknown as CancellationToken; -const mockLoggerFactory = { - get: jest.fn().mockImplementation(() => ({ - error: jest.fn(), - debug: jest.fn(), - warn: jest.fn(), - })), -}; -const getMockLogger = () => new LevelLogger(mockLoggerFactory); +const getMockLogger = () => loggingSystemMock.createLogger(); const mockEncryptionKey = 'abcabcsecuresecret'; const encryptHeaders = async (headers: Record) => { diff --git a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts index 51044aa324a1aa..5df7a497adf6c7 100644 --- a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts @@ -8,7 +8,7 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { finalize, map, mergeMap, takeUntil, tap } from 'rxjs/operators'; -import { PNG_JOB_TYPE_V2, REPORTING_TRANSACTION_TYPE } from '../../../common/constants'; +import { REPORTING_TRANSACTION_TYPE } from '../../../common/constants'; import { TaskRunResult } from '../../lib/tasks'; import { RunTaskFn, RunTaskFnFactory } from '../../types'; import { decryptJobHeaders, generatePngObservable } from '../common'; @@ -25,7 +25,7 @@ export const runTaskFnFactory: RunTaskFnFactory> = const apmGetAssets = apmTrans?.startSpan('get-assets', 'setup'); let apmGeneratePng: { end: () => void } | null | undefined; - const jobLogger = parentLogger.clone([PNG_JOB_TYPE_V2, 'execute', jobId]); + const jobLogger = parentLogger.get(`execute:${jobId}`); const process$: Rx.Observable = Rx.of(1).pipe( mergeMap(() => decryptJobHeaders(encryptionKey, job.headers, jobLogger)), mergeMap((headers) => { diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts index a8d2027f2ba120..7faa13486b5a18 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts @@ -6,10 +6,11 @@ */ import * as Rx from 'rxjs'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { Writable } from 'stream'; import { ReportingCore } from '../../../'; import { CancellationToken } from '../../../../common/cancellation_token'; -import { cryptoFactory, LevelLogger } from '../../../lib'; +import { cryptoFactory } from '../../../lib'; import { createMockConfigSchema, createMockReportingCore } from '../../../test_helpers'; import { generatePdfObservable } from '../lib/generate_pdf'; import { TaskPayloadPDF } from '../types'; @@ -25,14 +26,7 @@ const cancellationToken = { on: jest.fn(), } as unknown as CancellationToken; -const mockLoggerFactory = { - get: jest.fn().mockImplementation(() => ({ - error: jest.fn(), - debug: jest.fn(), - warn: jest.fn(), - })), -}; -const getMockLogger = () => new LevelLogger(mockLoggerFactory); +const getMockLogger = () => loggingSystemMock.createLogger(); const mockEncryptionKey = 'testencryptionkey'; const encryptHeaders = async (headers: Record) => { diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts index ab3793935e1d86..9b4db48ed66970 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts @@ -8,7 +8,7 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil, tap } from 'rxjs/operators'; -import { PDF_JOB_TYPE, REPORTING_TRANSACTION_TYPE } from '../../../../common/constants'; +import { REPORTING_TRANSACTION_TYPE } from '../../../../common/constants'; import { TaskRunResult } from '../../../lib/tasks'; import { RunTaskFn, RunTaskFnFactory } from '../../../types'; import { decryptJobHeaders, getFullUrls, getCustomLogo } from '../../common'; @@ -21,7 +21,7 @@ export const runTaskFnFactory: RunTaskFnFactory> = const encryptionKey = config.get('encryptionKey'); return async function runTask(jobId, job, cancellationToken, stream) { - const jobLogger = parentLogger.clone([PDF_JOB_TYPE, 'execute-job', jobId]); + const jobLogger = parentLogger.get(`execute-job:${jobId}`); const apmTrans = apm.startTransaction('execute-job-pdf', REPORTING_TRANSACTION_TYPE); const apmGetAssets = apmTrans?.startSpan('get-assets', 'setup'); let apmGeneratePdf: { end: () => void } | null | undefined; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts index a401f59b8f4bf0..ff0ef2cf39af4b 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts @@ -5,13 +5,13 @@ * 2.0. */ +import type { Logger } from 'kibana/server'; import { groupBy } from 'lodash'; import * as Rx from 'rxjs'; import { mergeMap, tap } from 'rxjs/operators'; +import { ReportingCore } from '../../../'; import { ScreenshotResult } from '../../../../../screenshotting/server'; import type { PdfMetrics } from '../../../../common/types'; -import { ReportingCore } from '../../../'; -import { LevelLogger } from '../../../lib'; import { ScreenshotOptions } from '../../../types'; import { PdfMaker } from '../../common/pdf'; import { getTracker } from './tracker'; @@ -34,7 +34,7 @@ interface PdfResult { export function generatePdfObservable( reporting: ReportingCore, - logger: LevelLogger, + logger: Logger, title: string, options: ScreenshotOptions, logo?: string diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.test.ts index 3cf7f82058563a..efad71a64a81d6 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.test.ts @@ -8,11 +8,12 @@ jest.mock('./lib/generate_pdf'); import * as Rx from 'rxjs'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { Writable } from 'stream'; import { ReportingCore } from '../../'; import { CancellationToken } from '../../../common/cancellation_token'; import { LocatorParams } from '../../../common/types'; -import { cryptoFactory, LevelLogger } from '../../lib'; +import { cryptoFactory } from '../../lib'; import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import { runTaskFnFactory } from './execute_job'; import { generatePdfObservable } from './lib/generate_pdf'; @@ -26,14 +27,7 @@ const cancellationToken = { on: jest.fn(), } as unknown as CancellationToken; -const mockLoggerFactory = { - get: jest.fn().mockImplementation(() => ({ - error: jest.fn(), - debug: jest.fn(), - warn: jest.fn(), - })), -}; -const getMockLogger = () => new LevelLogger(mockLoggerFactory); +const getMockLogger = () => loggingSystemMock.createLogger(); const mockEncryptionKey = 'testencryptionkey'; const encryptHeaders = async (headers: Record) => { diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts index 85684bca66b869..7f887707829cb1 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts @@ -8,7 +8,7 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil, tap } from 'rxjs/operators'; -import { PDF_JOB_TYPE_V2, REPORTING_TRANSACTION_TYPE } from '../../../common/constants'; +import { REPORTING_TRANSACTION_TYPE } from '../../../common/constants'; import { TaskRunResult } from '../../lib/tasks'; import { RunTaskFn, RunTaskFnFactory } from '../../types'; import { decryptJobHeaders, getCustomLogo } from '../common'; @@ -21,7 +21,7 @@ export const runTaskFnFactory: RunTaskFnFactory> = const encryptionKey = config.get('encryptionKey'); return async function runTask(jobId, job, cancellationToken, stream) { - const jobLogger = parentLogger.clone([PDF_JOB_TYPE_V2, 'execute-job', jobId]); + const jobLogger = parentLogger.get(`execute-job:${jobId}`); const apmTrans = apm.startTransaction('execute-job-pdf-v2', REPORTING_TRANSACTION_TYPE); const apmGetAssets = apmTrans?.startSpan('get-assets', 'setup'); let apmGeneratePdf: { end: () => void } | null | undefined; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts index ac922c07574b3c..8bec3cac28f430 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts @@ -5,14 +5,14 @@ * 2.0. */ +import type { Logger } from 'kibana/server'; import { groupBy } from 'lodash'; import * as Rx from 'rxjs'; import { mergeMap, tap } from 'rxjs/operators'; -import { ReportingCore } from '../../../'; -import { ScreenshotResult } from '../../../../../screenshotting/server'; -import { LocatorParams, PdfMetrics, UrlOrUrlLocatorTuple } from '../../../../common/types'; -import { LevelLogger } from '../../../lib'; -import { ScreenshotOptions } from '../../../types'; +import type { ReportingCore } from '../../../'; +import type { ScreenshotResult } from '../../../../../screenshotting/server'; +import type { LocatorParams, PdfMetrics, UrlOrUrlLocatorTuple } from '../../../../common/types'; +import type { ScreenshotOptions } from '../../../types'; import { PdfMaker } from '../../common/pdf'; import { getFullRedirectAppUrl } from '../../common/v2/get_full_redirect_app_url'; import type { TaskPayloadPDFV2 } from '../types'; @@ -36,7 +36,7 @@ interface PdfResult { export function generatePdfObservable( reporting: ReportingCore, - logger: LevelLogger, + logger: Logger, job: TaskPayloadPDFV2, title: string, locatorParams: LocatorParams[], diff --git a/x-pack/plugins/reporting/server/lib/check_params_version.ts b/x-pack/plugins/reporting/server/lib/check_params_version.ts index 7298384b875715..79237ba56677a5 100644 --- a/x-pack/plugins/reporting/server/lib/check_params_version.ts +++ b/x-pack/plugins/reporting/server/lib/check_params_version.ts @@ -5,16 +5,16 @@ * 2.0. */ +import type { Logger } from 'kibana/server'; import { UNVERSIONED_VERSION } from '../../common/constants'; import type { BaseParams } from '../../common/types'; -import type { LevelLogger } from './'; -export function checkParamsVersion(jobParams: BaseParams, logger: LevelLogger) { +export function checkParamsVersion(jobParams: BaseParams, logger: Logger) { if (jobParams.version) { logger.debug(`Using reporting job params v${jobParams.version}`); return jobParams.version; } - logger.warning(`No version provided in report job params. Assuming ${UNVERSIONED_VERSION}`); + logger.warn(`No version provided in report job params. Assuming ${UNVERSIONED_VERSION}`); return UNVERSIONED_VERSION; } diff --git a/x-pack/plugins/reporting/server/lib/content_stream.test.ts b/x-pack/plugins/reporting/server/lib/content_stream.test.ts index 0c45ef2d5f5ce8..069ac22258ad10 100644 --- a/x-pack/plugins/reporting/server/lib/content_stream.test.ts +++ b/x-pack/plugins/reporting/server/lib/content_stream.test.ts @@ -5,20 +5,20 @@ * 2.0. */ +import type { Logger } from 'kibana/server'; import { set } from 'lodash'; -import { elasticsearchServiceMock } from 'src/core/server/mocks'; -import { createMockLevelLogger } from '../test_helpers'; +import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; import { ContentStream } from './content_stream'; describe('ContentStream', () => { let client: ReturnType; - let logger: ReturnType; + let logger: Logger; let stream: ContentStream; let base64Stream: ContentStream; beforeEach(() => { client = elasticsearchServiceMock.createClusterClient().asInternalUser; - logger = createMockLevelLogger(); + logger = loggingSystemMock.createLogger(); stream = new ContentStream( client, logger, diff --git a/x-pack/plugins/reporting/server/lib/content_stream.ts b/x-pack/plugins/reporting/server/lib/content_stream.ts index c0b2d458b4d594..b09e446ff576c3 100644 --- a/x-pack/plugins/reporting/server/lib/content_stream.ts +++ b/x-pack/plugins/reporting/server/lib/content_stream.ts @@ -5,14 +5,13 @@ * 2.0. */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { Duplex } from 'stream'; +import { ByteSizeValue } from '@kbn/config-schema'; +import type { ElasticsearchClient, Logger } from 'kibana/server'; import { defaults, get } from 'lodash'; import Puid from 'puid'; -import { ByteSizeValue } from '@kbn/config-schema'; -import type { ElasticsearchClient } from 'src/core/server'; -import { ReportingCore } from '..'; -import { ReportSource } from '../../common/types'; -import { LevelLogger } from './level_logger'; +import { Duplex } from 'stream'; +import type { ReportingCore } from '../'; +import type { ReportSource } from '../../common/types'; /** * @note The Elasticsearch `http.max_content_length` is including the whole POST body. @@ -87,7 +86,7 @@ export class ContentStream extends Duplex { constructor( private client: ElasticsearchClient, - private logger: LevelLogger, + private logger: Logger, private document: ContentStreamDocument, { encoding = 'base64' }: ContentStreamParameters = {} ) { @@ -348,7 +347,7 @@ export async function getContentStream( return new ContentStream( client, - logger.clone(['content_stream', document.id]), + logger.get('content_stream').get(document.id), document, parameters ); diff --git a/x-pack/plugins/reporting/server/lib/event_logger/adapter.test.ts b/x-pack/plugins/reporting/server/lib/event_logger/adapter.test.ts index aef569a49e357d..90c546b198a08b 100644 --- a/x-pack/plugins/reporting/server/lib/event_logger/adapter.test.ts +++ b/x-pack/plugins/reporting/server/lib/event_logger/adapter.test.ts @@ -6,11 +6,11 @@ */ import { LogMeta } from 'kibana/server'; -import { createMockLevelLogger } from '../../test_helpers'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { EcsLogAdapter } from './adapter'; describe('EcsLogAdapter', () => { - const logger = createMockLevelLogger(); + const logger = loggingSystemMock.createLogger(); beforeAll(() => { jest .spyOn(global.Date, 'now') @@ -28,7 +28,7 @@ describe('EcsLogAdapter', () => { const event = { kibana: { reporting: { wins: 5000 } } } as object & LogMeta; // an object that extends LogMeta eventLogger.logEvent('hello world', event); - expect(logger.debug).toBeCalledWith('hello world', ['events'], { + expect(logger.debug).toBeCalledWith('hello world', { event: { duration: undefined, end: undefined, @@ -50,7 +50,7 @@ describe('EcsLogAdapter', () => { const event = { kibana: { reporting: { wins: 9000 } } } as object & LogMeta; // an object that extends LogMeta eventLogger.logEvent('hello duration', event); - expect(logger.debug).toBeCalledWith('hello duration', ['events'], { + expect(logger.debug).toBeCalledWith('hello duration', { event: { duration: 120000000000, end: '2021-04-12T16:02:00.000Z', diff --git a/x-pack/plugins/reporting/server/lib/event_logger/adapter.ts b/x-pack/plugins/reporting/server/lib/event_logger/adapter.ts index c9487a79d9e70e..71116d8f334b50 100644 --- a/x-pack/plugins/reporting/server/lib/event_logger/adapter.ts +++ b/x-pack/plugins/reporting/server/lib/event_logger/adapter.ts @@ -6,23 +6,26 @@ */ import deepMerge from 'deepmerge'; -import { LogMeta } from 'src/core/server'; -import { LevelLogger } from '../level_logger'; -import { IReportingEventLogger } from './logger'; +import type { Logger, LogMeta } from 'kibana/server'; +import type { IReportingEventLogger } from './logger'; /** @internal */ export class EcsLogAdapter implements IReportingEventLogger { start?: Date; end?: Date; + private logger: Logger; + /** * This class provides a logging system to Reporting code, using a shape similar to the EventLog service. * The logging action causes ECS data with Reporting metrics sent to DEBUG logs. * - * @param {LevelLogger} logger - Reporting's wrapper of the core logger + * @param {Logger} logger - Reporting's wrapper of the core logger * @param {Partial} properties - initial ECS data with template for Reporting metrics */ - constructor(private logger: LevelLogger, private properties: Partial) {} + constructor(logger: Logger, private properties: Partial) { + this.logger = logger.get('events'); + } logEvent(message: string, properties: LogMeta) { if (this.start && !this.end) { @@ -44,7 +47,7 @@ export class EcsLogAdapter implements IReportingEventLogger { }); // sends an ECS object with Reporting metrics to the DEBUG logs - this.logger.debug(message, ['events'], deepMerge(newProperties, properties)); + this.logger.debug(message, deepMerge(newProperties, properties)); } startTiming() { diff --git a/x-pack/plugins/reporting/server/lib/event_logger/logger.test.ts b/x-pack/plugins/reporting/server/lib/event_logger/logger.test.ts index fa45a8d04176c8..c58777747c3fd3 100644 --- a/x-pack/plugins/reporting/server/lib/event_logger/logger.test.ts +++ b/x-pack/plugins/reporting/server/lib/event_logger/logger.test.ts @@ -5,8 +5,8 @@ * 2.0. */ +import { loggingSystemMock } from 'src/core/server/mocks'; import { ConcreteTaskInstance } from '../../../../task_manager/server'; -import { createMockLevelLogger } from '../../test_helpers'; import { BasePayload } from '../../types'; import { Report } from '../store'; import { ReportingEventLogger, reportingEventLoggerFactory } from './logger'; @@ -21,7 +21,7 @@ describe('Event Logger', () => { let factory: ReportingEventLogger; beforeEach(() => { - factory = reportingEventLoggerFactory(createMockLevelLogger()); + factory = reportingEventLoggerFactory(loggingSystemMock.createLogger()); }); it(`should construct with an internal seed object`, () => { diff --git a/x-pack/plugins/reporting/server/lib/event_logger/logger.ts b/x-pack/plugins/reporting/server/lib/event_logger/logger.ts index 6a7feea0c335d0..965a55e24229a2 100644 --- a/x-pack/plugins/reporting/server/lib/event_logger/logger.ts +++ b/x-pack/plugins/reporting/server/lib/event_logger/logger.ts @@ -6,8 +6,7 @@ */ import deepMerge from 'deepmerge'; -import { LogMeta } from 'src/core/server'; -import { LevelLogger } from '../'; +import type { Logger, LogMeta } from 'kibana/server'; import { PLUGIN_ID } from '../../../common/constants'; import type { TaskRunMetrics } from '../../../common/types'; import { IReport } from '../store'; @@ -46,7 +45,7 @@ export interface BaseEvent { } /** @internal */ -export function reportingEventLoggerFactory(logger: LevelLogger) { +export function reportingEventLoggerFactory(logger: Logger) { const genericLogger = new EcsLogAdapter(logger, { event: { provider: PLUGIN_ID } }); return class ReportingEventLogger { diff --git a/x-pack/plugins/reporting/server/lib/index.ts b/x-pack/plugins/reporting/server/lib/index.ts index 682f547380ba0d..36d310fcd131b1 100644 --- a/x-pack/plugins/reporting/server/lib/index.ts +++ b/x-pack/plugins/reporting/server/lib/index.ts @@ -10,7 +10,6 @@ export { checkParamsVersion } from './check_params_version'; export { ContentStream, getContentStream } from './content_stream'; export { cryptoFactory } from './crypto'; export { ExportTypesRegistry, getExportTypesRegistry } from './export_types_registry'; -export { LevelLogger } from './level_logger'; export { PassThroughStream } from './passthrough_stream'; export { statuses } from './statuses'; export { ReportingStore, IlmPolicyManager } from './store'; diff --git a/x-pack/plugins/reporting/server/lib/level_logger.ts b/x-pack/plugins/reporting/server/lib/level_logger.ts deleted file mode 100644 index 91cf6757dbee22..00000000000000 --- a/x-pack/plugins/reporting/server/lib/level_logger.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { LoggerFactory, LogMeta } from 'src/core/server'; - -const trimStr = (toTrim: string) => { - return typeof toTrim === 'string' ? toTrim.trim() : toTrim; -}; - -export interface GenericLevelLogger { - debug: (msg: string, tags: string[], meta: T) => void; - info: (msg: string) => void; - warning: (msg: string) => void; - error: (msg: Error) => void; -} - -export class LevelLogger implements GenericLevelLogger { - private _logger: LoggerFactory; - private _tags: string[]; - public warning: (msg: string, tags?: string[]) => void; - - constructor(logger: LoggerFactory, tags?: string[]) { - this._logger = logger; - this._tags = tags || []; - - /* - * This shortcut provides maintenance convenience: Reporting code has been - * using both .warn and .warning - */ - this.warning = this.warn.bind(this); - } - - private getLogger(tags: string[]) { - return this._logger.get(...this._tags, ...tags); - } - - public error(err: string | Error, tags: string[] = []) { - this.getLogger(tags).error(err); - } - - public warn(msg: string, tags: string[] = []) { - this.getLogger(tags).warn(msg); - } - - // only "debug" logging supports the LogMeta for now... - public debug(msg: string, tags: string[] = [], meta?: T) { - this.getLogger(tags).debug(msg, meta); - } - - public trace(msg: string, tags: string[] = []) { - this.getLogger(tags).trace(msg); - } - - public info(msg: string, tags: string[] = []) { - this.getLogger(tags).info(trimStr(msg)); - } - - public clone(tags: string[]) { - return new LevelLogger(this._logger, [...this._tags, ...tags]); - } -} diff --git a/x-pack/plugins/reporting/server/lib/store/store.test.ts b/x-pack/plugins/reporting/server/lib/store/store.test.ts index 3e8942be1ffa0b..7ceafef261dd49 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.test.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.test.ts @@ -5,17 +5,13 @@ * 2.0. */ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { elasticsearchServiceMock } from 'src/core/server/mocks'; +import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; import { ReportingCore } from '../../'; -import { - createMockConfigSchema, - createMockLevelLogger, - createMockReportingCore, -} from '../../test_helpers'; +import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import { Report, ReportDocument, ReportingStore, SavedReport } from './'; describe('ReportingStore', () => { - const mockLogger = createMockLevelLogger(); + const mockLogger = loggingSystemMock.createLogger(); let mockCore: ReportingCore; let mockEsClient: ReturnType; diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts index 41fdd9580c996c..7e920e718d51ee 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.ts @@ -6,13 +6,14 @@ */ import { IndexResponse, UpdateResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { ElasticsearchClient } from 'src/core/server'; -import { LevelLogger, statuses } from '../'; -import { ReportingCore } from '../../'; +import type { ElasticsearchClient, Logger } from 'kibana/server'; +import { statuses } from '../'; +import type { ReportingCore } from '../../'; import { ILM_POLICY_NAME, REPORTING_SYSTEM_INDEX } from '../../../common/constants'; -import { JobStatus, ReportOutput, ReportSource } from '../../../common/types'; -import { ReportTaskParams } from '../tasks'; -import { IReport, Report, ReportDocument, SavedReport } from './'; +import type { JobStatus, ReportOutput, ReportSource } from '../../../common/types'; +import type { ReportTaskParams } from '../tasks'; +import type { IReport, Report, ReportDocument } from './'; +import { SavedReport } from './'; import { IlmPolicyManager } from './ilm_policy_manager'; import { indexTimestamp } from './index_timestamp'; import { mapping } from './mapping'; @@ -83,12 +84,12 @@ export class ReportingStore { private client?: ElasticsearchClient; private ilmPolicyManager?: IlmPolicyManager; - constructor(private reportingCore: ReportingCore, private logger: LevelLogger) { + constructor(private reportingCore: ReportingCore, private logger: Logger) { const config = reportingCore.getConfig(); this.indexPrefix = REPORTING_SYSTEM_INDEX; this.indexInterval = config.get('queue', 'indexInterval'); - this.logger = logger.clone(['store']); + this.logger = logger.get('store'); } private async getClient() { diff --git a/x-pack/plugins/reporting/server/lib/tasks/error_logger.test.ts b/x-pack/plugins/reporting/server/lib/tasks/error_logger.test.ts index 607c9c32538be4..302088e6a6eb13 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/error_logger.test.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/error_logger.test.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { createMockLevelLogger } from '../../test_helpers'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { errorLogger } from './error_logger'; -const logger = createMockLevelLogger(); +const logger = loggingSystemMock.createLogger(); describe('Execute Report Error Logger', () => { const errorLogSpy = jest.spyOn(logger, 'error'); diff --git a/x-pack/plugins/reporting/server/lib/tasks/error_logger.ts b/x-pack/plugins/reporting/server/lib/tasks/error_logger.ts index b4d4028230666a..a67e3caeb2c78c 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/error_logger.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/error_logger.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { LevelLogger } from '..'; +import type { Logger } from 'kibana/server'; const MAX_PARTIAL_ERROR_LENGTH = 1000; // 1000 of beginning, 1000 of end const ERROR_PARTIAL_SEPARATOR = '...'; @@ -15,7 +15,7 @@ const MAX_ERROR_LENGTH = MAX_PARTIAL_ERROR_LENGTH * 2 + ERROR_PARTIAL_SEPARATOR. * An error message string could be very long, as it sometimes includes huge * amount of base64 */ -export const errorLogger = (logger: LevelLogger, message: string, err?: Error) => { +export const errorLogger = (logger: Logger, message: string, err?: Error) => { if (err) { const errString = `${message}: ${err}`; const errLength = errString.length; diff --git a/x-pack/plugins/reporting/server/lib/tasks/execute_report.test.ts b/x-pack/plugins/reporting/server/lib/tasks/execute_report.test.ts index df662d963d0edf..b47df99b7a0fde 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/execute_report.test.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/execute_report.test.ts @@ -5,18 +5,15 @@ * 2.0. */ +import { loggingSystemMock } from 'src/core/server/mocks'; import { ReportingCore } from '../..'; import { RunContext } from '../../../../task_manager/server'; import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { ReportingConfigType } from '../../config'; -import { - createMockConfigSchema, - createMockLevelLogger, - createMockReportingCore, -} from '../../test_helpers'; +import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import { ExecuteReportTask } from './'; -const logger = createMockLevelLogger(); +const logger = loggingSystemMock.createLogger(); describe('Execute Report Task', () => { let mockReporting: ReportingCore; diff --git a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts index 449f3b8da7671a..4d4959eef00c44 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts @@ -6,20 +6,21 @@ */ import { UpdateResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { Logger } from 'kibana/server'; import moment from 'moment'; import * as Rx from 'rxjs'; import { timeout } from 'rxjs/operators'; import { finished, Writable } from 'stream'; import { promisify } from 'util'; -import { getContentStream, LevelLogger } from '../'; -import { ReportingCore } from '../../'; -import { +import { getContentStream } from '../'; +import type { ReportingCore } from '../../'; +import type { RunContext, TaskManagerStartContract, TaskRunCreatorFunction, } from '../../../../task_manager/server'; import { CancellationToken } from '../../../common/cancellation_token'; -import { ReportingError, UnknownError, QueueTimeoutError } from '../../../common/errors'; +import { QueueTimeoutError, ReportingError, UnknownError } from '../../../common/errors'; import { durationToNumber, numberToDuration } from '../../../common/schema_utils'; import type { ReportOutput } from '../../../common/types'; import type { ReportingConfigType } from '../../config'; @@ -60,7 +61,7 @@ function reportFromTask(task: ReportTaskParams) { export class ExecuteReportTask implements ReportingTask { public TYPE = REPORTING_EXECUTE_TYPE; - private logger: LevelLogger; + private logger: Logger; private taskManagerStart?: TaskManagerStartContract; private taskExecutors?: Map; private kibanaId?: string; @@ -70,9 +71,9 @@ export class ExecuteReportTask implements ReportingTask { constructor( private reporting: ReportingCore, private config: ReportingConfigType, - logger: LevelLogger + logger: Logger ) { - this.logger = logger.clone(['runTask']); + this.logger = logger.get('runTask'); } /* @@ -86,7 +87,7 @@ export class ExecuteReportTask implements ReportingTask { const exportTypesRegistry = reporting.getExportTypesRegistry(); const executors = new Map(); for (const exportType of exportTypesRegistry.getAll()) { - const exportTypeLogger = this.logger.clone([exportType.id]); + const exportTypeLogger = this.logger.get(exportType.jobType); const jobExecutor = exportType.runTaskFnFactory(reporting, exportTypeLogger); // The task will run the function with the job type as a param. // This allows us to retrieve the specific export type runFn when called to run an export @@ -476,7 +477,7 @@ export class ExecuteReportTask implements ReportingTask { return await this.getTaskManagerStart().schedule(taskInstance); } - private async rescheduleTask(task: ReportTaskParams, logger: LevelLogger) { + private async rescheduleTask(task: ReportTaskParams, logger: Logger) { logger.info(`Rescheduling task:${task.id} to retry after error.`); const oldTaskInstance: ReportingExecuteTaskInstance = { diff --git a/x-pack/plugins/reporting/server/lib/tasks/monitor_report.test.ts b/x-pack/plugins/reporting/server/lib/tasks/monitor_report.test.ts index d737c7032855b9..b7e75de2475358 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/monitor_report.test.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/monitor_report.test.ts @@ -5,18 +5,15 @@ * 2.0. */ +import { loggingSystemMock } from 'src/core/server/mocks'; import { ReportingCore } from '../..'; import { RunContext } from '../../../../task_manager/server'; import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { ReportingConfigType } from '../../config'; -import { - createMockConfigSchema, - createMockLevelLogger, - createMockReportingCore, -} from '../../test_helpers'; +import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import { MonitorReportsTask } from './'; -const logger = createMockLevelLogger(); +const logger = loggingSystemMock.createLogger(); describe('Execute Report Task', () => { let mockReporting: ReportingCore; diff --git a/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts b/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts index 4af28e3d1a6981..1d406d7a5cc623 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts @@ -5,8 +5,9 @@ * 2.0. */ +import type { Logger } from 'kibana/server'; import moment from 'moment'; -import { LevelLogger, ReportingStore } from '../'; +import { ReportingStore } from '../'; import { ReportingCore } from '../../'; import { TaskManagerStartContract, TaskRunCreatorFunction } from '../../../../task_manager/server'; import { numberToDuration } from '../../../common/schema_utils'; @@ -38,7 +39,7 @@ import { ReportingTask, ReportingTaskStatus, REPORTING_MONITOR_TYPE, ReportTaskP export class MonitorReportsTask implements ReportingTask { public TYPE = REPORTING_MONITOR_TYPE; - private logger: LevelLogger; + private logger: Logger; private taskManagerStart?: TaskManagerStartContract; private store?: ReportingStore; private timeout: moment.Duration; @@ -46,9 +47,9 @@ export class MonitorReportsTask implements ReportingTask { constructor( private reporting: ReportingCore, private config: ReportingConfigType, - parentLogger: LevelLogger + parentLogger: Logger ) { - this.logger = parentLogger.clone([REPORTING_MONITOR_TYPE]); + this.logger = parentLogger.get(REPORTING_MONITOR_TYPE); this.timeout = numberToDuration(config.queue.timeout); } @@ -145,7 +146,7 @@ export class MonitorReportsTask implements ReportingTask { } // reschedule the task with TM - private async rescheduleTask(task: ReportTaskParams, logger: LevelLogger) { + private async rescheduleTask(task: ReportTaskParams, logger: Logger) { if (!this.taskManagerStart) { throw new Error('Reporting task runner has not been initialized!'); } diff --git a/x-pack/plugins/reporting/server/plugin.test.ts b/x-pack/plugins/reporting/server/plugin.test.ts index e179d847d95260..98f02668323b1a 100644 --- a/x-pack/plugins/reporting/server/plugin.test.ts +++ b/x-pack/plugins/reporting/server/plugin.test.ts @@ -5,14 +5,12 @@ * 2.0. */ -import type { CoreSetup, CoreStart } from 'kibana/server'; -import { coreMock } from 'src/core/server/mocks'; +import type { CoreSetup, CoreStart, Logger } from 'kibana/server'; +import { coreMock, loggingSystemMock } from 'src/core/server/mocks'; import type { ReportingCore, ReportingInternalStart } from './core'; -import { LevelLogger } from './lib'; import { ReportingPlugin } from './plugin'; import { createMockConfigSchema, - createMockLevelLogger, createMockPluginSetup, createMockPluginStart, } from './test_helpers'; @@ -27,7 +25,7 @@ describe('Reporting Plugin', () => { let coreStart: CoreStart; let pluginSetup: ReportingSetupDeps; let pluginStart: ReportingInternalStart; - let logger: jest.Mocked; + let logger: jest.Mocked; let plugin: ReportingPlugin; beforeEach(async () => { @@ -38,9 +36,9 @@ describe('Reporting Plugin', () => { pluginSetup = createMockPluginSetup({}) as unknown as ReportingSetupDeps; pluginStart = await createMockPluginStart(coreStart, configSchema); - logger = createMockLevelLogger(); + logger = loggingSystemMock.createLogger(); plugin = new ReportingPlugin(initContext); - (plugin as unknown as { logger: LevelLogger }).logger = logger; + (plugin as unknown as { logger: Logger }).logger = logger; }); it('has a sync setup process', () => { diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index a0d4bfed7c7e0a..37d6494f5e079b 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -5,12 +5,12 @@ * 2.0. */ -import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server'; +import type { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; import { PLUGIN_ID } from '../common/constants'; import { ReportingCore } from './'; import { buildConfig, registerUiSettings, ReportingConfigType } from './config'; import { registerDeprecations } from './deprecations'; -import { LevelLogger, ReportingStore } from './lib'; +import { ReportingStore } from './lib'; import { registerRoutes } from './routes'; import { setFieldFormats } from './services'; import type { @@ -28,11 +28,11 @@ import { registerReportingUsageCollector } from './usage'; export class ReportingPlugin implements Plugin { - private logger: LevelLogger; + private logger: Logger; private reportingCore?: ReportingCore; constructor(private initContext: PluginInitializerContext) { - this.logger = new LevelLogger(initContext.logger.get()); + this.logger = initContext.logger.get(); } public setup(core: CoreSetup, plugins: ReportingSetupDeps) { diff --git a/x-pack/plugins/reporting/server/routes/deprecations/deprecations.ts b/x-pack/plugins/reporting/server/routes/deprecations/deprecations.ts index 4c368337cd4822..89d55ff04ab8fd 100644 --- a/x-pack/plugins/reporting/server/routes/deprecations/deprecations.ts +++ b/x-pack/plugins/reporting/server/routes/deprecations/deprecations.ts @@ -5,16 +5,16 @@ * 2.0. */ import { errors } from '@elastic/elasticsearch'; -import { SecurityHasPrivilegesIndexPrivilegesCheck } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { RequestHandler } from 'src/core/server'; +import type { SecurityHasPrivilegesIndexPrivilegesCheck } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { Logger, RequestHandler } from 'kibana/server'; import { API_GET_ILM_POLICY_STATUS, API_MIGRATE_ILM_POLICY_URL, ILM_POLICY_NAME, } from '../../../common/constants'; -import { IlmPolicyStatusResponse } from '../../../common/types'; -import { ReportingCore } from '../../core'; -import { IlmPolicyManager, LevelLogger as Logger } from '../../lib'; +import type { IlmPolicyStatusResponse } from '../../../common/types'; +import type { ReportingCore } from '../../core'; +import { IlmPolicyManager } from '../../lib'; import { deprecations } from '../../lib/deprecations'; export const registerDeprecationsRoutes = (reporting: ReportingCore, logger: Logger) => { diff --git a/x-pack/plugins/reporting/server/routes/deprecations/integration_tests/deprecations.test.ts b/x-pack/plugins/reporting/server/routes/deprecations/integration_tests/deprecations.test.ts index 67d7d0c4a0c080..9c76aade058f0e 100644 --- a/x-pack/plugins/reporting/server/routes/deprecations/integration_tests/deprecations.test.ts +++ b/x-pack/plugins/reporting/server/routes/deprecations/integration_tests/deprecations.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { loggingSystemMock } from 'src/core/server/mocks'; import { setupServer } from 'src/core/server/test_utils'; import supertest from 'supertest'; import { licensingMock } from '../../../../../licensing/server/mocks'; @@ -12,7 +13,6 @@ import { securityMock } from '../../../../../security/server/mocks'; import { API_GET_ILM_POLICY_STATUS } from '../../../../common/constants'; import { createMockConfigSchema, - createMockLevelLogger, createMockPluginSetup, createMockPluginStart, createMockReportingCore, @@ -54,7 +54,7 @@ describe(`GET ${API_GET_ILM_POLICY_STATUS}`, () => { it('correctly handles authz when security is unavailable', async () => { const core = await createReportingCore({}); - registerDeprecationsRoutes(core, createMockLevelLogger()); + registerDeprecationsRoutes(core, loggingSystemMock.createLogger()); await server.start(); await supertest(httpSetup.server.listener) @@ -68,7 +68,7 @@ describe(`GET ${API_GET_ILM_POLICY_STATUS}`, () => { security.license.isEnabled.mockReturnValue(false); const core = await createReportingCore({ security }); - registerDeprecationsRoutes(core, createMockLevelLogger()); + registerDeprecationsRoutes(core, loggingSystemMock.createLogger()); await server.start(); await supertest(httpSetup.server.listener) diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts b/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts index f68df294b41183..fb95ad9e318804 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts @@ -6,11 +6,11 @@ */ import { i18n } from '@kbn/i18n'; -import { ReportingCore } from '../..'; +import type { Logger } from 'kibana/server'; +import type { ReportingCore } from '../..'; import { API_DIAGNOSE_URL } from '../../../common/constants'; -import { LevelLogger as Logger } from '../../lib'; import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing'; -import { DiagnosticResponse } from './'; +import type { DiagnosticResponse } from './'; const logsToHelpMap = { 'error while loading shared libraries': i18n.translate( diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/index.ts b/x-pack/plugins/reporting/server/routes/diagnostic/index.ts index 92404b76e07418..b5e2a8585afb32 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/index.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/index.ts @@ -5,10 +5,10 @@ * 2.0. */ +import type { Logger } from 'kibana/server'; +import type { ReportingCore } from '../../core'; import { registerDiagnoseBrowser } from './browser'; import { registerDiagnoseScreenshot } from './screenshot'; -import { LevelLogger as Logger } from '../../lib'; -import { ReportingCore } from '../../core'; export const registerDiagnosticRoutes = (reporting: ReportingCore, logger: Logger) => { registerDiagnoseBrowser(reporting, logger); diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/browser.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/browser.test.ts index 911807e63a9d56..dc8fdb7e6d0c87 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/browser.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/browser.test.ts @@ -6,13 +6,13 @@ */ import * as Rx from 'rxjs'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { setupServer } from 'src/core/server/test_utils'; import supertest from 'supertest'; import { ReportingCore } from '../../../'; import type { ScreenshottingStart } from '../../../../../screenshotting/server'; import { createMockConfigSchema, - createMockLevelLogger, createMockPluginSetup, createMockReportingCore, } from '../../../test_helpers'; @@ -27,7 +27,7 @@ const fontNotFoundMessage = 'Could not find the default font'; describe('POST /diagnose/browser', () => { jest.setTimeout(6000); const reportingSymbol = Symbol('reporting'); - const mockLogger = createMockLevelLogger(); + const mockLogger = loggingSystemMock.createLogger(); let server: SetupServerReturn['server']; let httpSetup: SetupServerReturn['httpSetup']; diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/screenshot.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/screenshot.test.ts index ad90679e67adb8..3bc3f5bbb5e287 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/screenshot.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/screenshot.test.ts @@ -5,13 +5,13 @@ * 2.0. */ +import { loggingSystemMock } from 'src/core/server/mocks'; import { setupServer } from 'src/core/server/test_utils'; import supertest from 'supertest'; import { ReportingCore } from '../../../'; import { generatePngObservable } from '../../../export_types/common'; import { createMockConfigSchema, - createMockLevelLogger, createMockPluginSetup, createMockReportingCore, } from '../../../test_helpers'; @@ -38,7 +38,7 @@ describe('POST /diagnose/screenshot', () => { }; const config = createMockConfigSchema({ queue: { timeout: 120000 } }); - const mockLogger = createMockLevelLogger(); + const mockLogger = loggingSystemMock.createLogger(); beforeEach(async () => { ({ server, httpSetup } = await setupServer(reportingSymbol)); diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts index 90b4c9d9a30c6f..6819970fe753a9 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts @@ -6,12 +6,12 @@ */ import { i18n } from '@kbn/i18n'; +import type { Logger } from 'kibana/server'; import { ReportingCore } from '../..'; import { APP_WRAPPER_CLASS } from '../../../../../../src/core/server'; import { API_DIAGNOSE_URL } from '../../../common/constants'; import { generatePngObservable } from '../../export_types/common'; import { getAbsoluteUrlFactory } from '../../export_types/common/get_absolute_url'; -import { LevelLogger as Logger } from '../../lib'; import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing'; import { DiagnosticResponse } from './'; diff --git a/x-pack/plugins/reporting/server/routes/generate/csv_searchsource_immediate.ts b/x-pack/plugins/reporting/server/routes/generate/csv_searchsource_immediate.ts index b6ada00ba55ab7..19687b9d3ec9b8 100644 --- a/x-pack/plugins/reporting/server/routes/generate/csv_searchsource_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/generate/csv_searchsource_immediate.ts @@ -6,13 +6,13 @@ */ import { schema } from '@kbn/config-schema'; -import { KibanaRequest } from 'src/core/server'; -import { ReportingCore } from '../../'; +import type { KibanaRequest, Logger } from 'kibana/server'; +import type { ReportingCore } from '../../'; import { CSV_SEARCHSOURCE_IMMEDIATE_TYPE } from '../../../common/constants'; import { runTaskFnFactory } from '../../export_types/csv_searchsource_immediate/execute_job'; -import { JobParamsDownloadCSV } from '../../export_types/csv_searchsource_immediate/types'; -import { LevelLogger as Logger, PassThroughStream } from '../../lib'; -import { BaseParams } from '../../types'; +import type { JobParamsDownloadCSV } from '../../export_types/csv_searchsource_immediate/types'; +import { PassThroughStream } from '../../lib'; +import type { BaseParams } from '../../types'; import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing'; import { RequestHandler } from '../lib/request_handler'; @@ -64,7 +64,7 @@ export function registerGenerateCsvFromSavedObjectImmediate( authorizedUserPreRouting( reporting, async (user, context, req: CsvFromSavedObjectRequest, res) => { - const logger = parentLogger.clone([CSV_SEARCHSOURCE_IMMEDIATE_TYPE]); + const logger = parentLogger.get(CSV_SEARCHSOURCE_IMMEDIATE_TYPE); const runTaskFn = runTaskFnFactory(reporting, logger); const requestHandler = new RequestHandler(reporting, user, context, req, res, logger); const stream = new PassThroughStream(); diff --git a/x-pack/plugins/reporting/server/routes/generate/generate_from_jobparams.ts b/x-pack/plugins/reporting/server/routes/generate/generate_from_jobparams.ts index cfcb7d6d2b05c6..c5e7bb2197d722 100644 --- a/x-pack/plugins/reporting/server/routes/generate/generate_from_jobparams.ts +++ b/x-pack/plugins/reporting/server/routes/generate/generate_from_jobparams.ts @@ -7,16 +7,16 @@ import { schema } from '@kbn/config-schema'; import rison from 'rison-node'; -import { ReportingCore } from '../..'; +import type { Logger } from 'kibana/server'; +import type { ReportingCore } from '../..'; import { API_BASE_URL } from '../../../common/constants'; -import { LevelLogger } from '../../lib'; -import { BaseParams } from '../../types'; +import type { BaseParams } from '../../types'; import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing'; import { RequestHandler } from '../lib/request_handler'; const BASE_GENERATE = `${API_BASE_URL}/generate`; -export function registerJobGenerationRoutes(reporting: ReportingCore, logger: LevelLogger) { +export function registerJobGenerationRoutes(reporting: ReportingCore, logger: Logger) { const setupDeps = reporting.getPluginSetupDeps(); const { router } = setupDeps; diff --git a/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts b/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts index f6db9e92086eb2..f0db06485cf448 100644 --- a/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts +++ b/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts @@ -7,6 +7,7 @@ import rison from 'rison-node'; import { BehaviorSubject } from 'rxjs'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { setupServer } from 'src/core/server/test_utils'; import supertest from 'supertest'; import { ReportingCore } from '../../../'; @@ -16,7 +17,6 @@ import { ExportTypesRegistry } from '../../../lib/export_types_registry'; import { Report } from '../../../lib/store'; import { createMockConfigSchema, - createMockLevelLogger, createMockPluginSetup, createMockPluginStart, createMockReportingCore, @@ -38,7 +38,7 @@ describe('POST /api/reporting/generate', () => { queue: { indexInterval: 'year', timeout: 10000, pollEnabled: true }, }); - const mockLogger = createMockLevelLogger(); + const mockLogger = loggingSystemMock.createLogger(); beforeEach(async () => { ({ server, httpSetup } = await setupServer(reportingSymbol)); diff --git a/x-pack/plugins/reporting/server/routes/index.ts b/x-pack/plugins/reporting/server/routes/index.ts index 49f602062b0c16..0cc0d1bdc67967 100644 --- a/x-pack/plugins/reporting/server/routes/index.ts +++ b/x-pack/plugins/reporting/server/routes/index.ts @@ -5,8 +5,8 @@ * 2.0. */ +import type { Logger } from 'kibana/server'; import { ReportingCore } from '..'; -import { LevelLogger } from '../lib'; import { registerDeprecationsRoutes } from './deprecations/deprecations'; import { registerDiagnosticRoutes } from './diagnostic'; import { @@ -15,7 +15,7 @@ import { } from './generate'; import { registerJobInfoRoutes } from './management'; -export function registerRoutes(reporting: ReportingCore, logger: LevelLogger) { +export function registerRoutes(reporting: ReportingCore, logger: Logger) { registerDeprecationsRoutes(reporting, logger); registerDiagnosticRoutes(reporting, logger); registerGenerateCsvFromSavedObjectImmediate(reporting, logger); diff --git a/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts b/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts index 7f4d85ff141560..27126baad021d6 100644 --- a/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts +++ b/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts @@ -138,7 +138,7 @@ export function jobsQueryFactory(reportingCore: ReportingCore): JobsQueryFactory async get(user, id) { const { logger } = reportingCore.getPluginSetupDeps(); if (!id) { - logger.warning(`No ID provided for GET`); + logger.warn(`No ID provided for GET`); return; } @@ -163,7 +163,7 @@ export function jobsQueryFactory(reportingCore: ReportingCore): JobsQueryFactory const result = response?.hits?.hits?.[0]; if (!result?._source) { - logger.warning(`No hits resulted in search`); + logger.warn(`No hits resulted in search`); return; } diff --git a/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts b/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts index d1c1dddb3c3021..c97ec3285839df 100644 --- a/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts +++ b/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts @@ -6,16 +6,12 @@ */ import { KibanaRequest, KibanaResponseFactory } from 'kibana/server'; -import { coreMock, httpServerMock } from 'src/core/server/mocks'; +import { coreMock, httpServerMock, loggingSystemMock } from 'src/core/server/mocks'; import { ReportingCore } from '../..'; import { JobParamsPDFDeprecated, TaskPayloadPDF } from '../../export_types/printable_pdf/types'; import { Report, ReportingStore } from '../../lib/store'; import { ReportApiJSON } from '../../lib/store/report'; -import { - createMockConfigSchema, - createMockLevelLogger, - createMockReportingCore, -} from '../../test_helpers'; +import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import { ReportingRequestHandlerContext, ReportingSetup } from '../../types'; import { RequestHandler } from './request_handler'; @@ -43,7 +39,7 @@ const getMockResponseFactory = () => unauthorized: (obj: unknown) => obj, } as unknown as KibanaResponseFactory); -const mockLogger = createMockLevelLogger(); +const mockLogger = loggingSystemMock.createLogger(); describe('Handle request to generate', () => { let reportingCore: ReportingCore; diff --git a/x-pack/plugins/reporting/server/routes/lib/request_handler.ts b/x-pack/plugins/reporting/server/routes/lib/request_handler.ts index b0a2032c18f19e..b8a3a4c69802ca 100644 --- a/x-pack/plugins/reporting/server/routes/lib/request_handler.ts +++ b/x-pack/plugins/reporting/server/routes/lib/request_handler.ts @@ -7,12 +7,12 @@ import Boom from '@hapi/boom'; import { i18n } from '@kbn/i18n'; -import { KibanaRequest, KibanaResponseFactory } from 'kibana/server'; -import { ReportingCore } from '../..'; +import type { KibanaRequest, KibanaResponseFactory, Logger } from 'kibana/server'; +import type { ReportingCore } from '../..'; import { API_BASE_URL } from '../../../common/constants'; -import { checkParamsVersion, cryptoFactory, LevelLogger } from '../../lib'; +import { checkParamsVersion, cryptoFactory } from '../../lib'; import { Report } from '../../lib/store'; -import { BaseParams, ReportingRequestHandlerContext, ReportingUser } from '../../types'; +import type { BaseParams, ReportingRequestHandlerContext, ReportingUser } from '../../types'; export const handleUnavailable = (res: KibanaResponseFactory) => { return res.custom({ statusCode: 503, body: 'Not Available' }); @@ -30,7 +30,7 @@ export class RequestHandler { private context: ReportingRequestHandlerContext, private req: KibanaRequest, private res: KibanaResponseFactory, - private logger: LevelLogger + private logger: Logger ) {} private async encryptHeaders() { @@ -53,7 +53,7 @@ export class RequestHandler { } const [createJob, store] = await Promise.all([ - exportType.createJobFnFactory(reporting, logger.clone([exportType.id])), + exportType.createJobFnFactory(reporting, logger.get(exportType.id)), reporting.getStore(), ]); diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_levellogger.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_levellogger.ts deleted file mode 100644 index a6e6be47bdfcdd..00000000000000 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_levellogger.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -jest.mock('../lib/level_logger'); - -import { loggingSystemMock } from 'src/core/server/mocks'; -import { LevelLogger } from '../lib/level_logger'; - -export function createMockLevelLogger() { - // eslint-disable-next-line no-console - const consoleLogger = (tag: string) => (message: unknown) => console.log(tag, message); - - const logger = new LevelLogger(loggingSystemMock.create()) as jest.Mocked; - - // logger.debug.mockImplementation(consoleLogger('debug')); // uncomment this to see debug logs in jest tests - logger.info.mockImplementation(consoleLogger('info')); - logger.warn.mockImplementation(consoleLogger('warn')); - logger.warning = jest.fn().mockImplementation(consoleLogger('warn')); - logger.error.mockImplementation(consoleLogger('error')); - logger.trace.mockImplementation(consoleLogger('trace')); - - logger.clone.mockImplementation(() => logger); - - return logger; -} diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts index 49d92a0fe4448e..e00ebd99f0420a 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts @@ -10,7 +10,12 @@ jest.mock('../usage'); import _ from 'lodash'; import { BehaviorSubject } from 'rxjs'; -import { coreMock, elasticsearchServiceMock, statusServiceMock } from 'src/core/server/mocks'; +import { + coreMock, + elasticsearchServiceMock, + loggingSystemMock, + statusServiceMock, +} from 'src/core/server/mocks'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { dataPluginMock } from 'src/plugins/data/server/mocks'; import { FieldFormatsRegistry } from 'src/plugins/field_formats/common'; @@ -27,7 +32,6 @@ import { buildConfig, ReportingConfigType } from '../config'; import { ReportingInternalSetup, ReportingInternalStart } from '../core'; import { ReportingStore } from '../lib'; import { setFieldFormats } from '../services'; -import { createMockLevelLogger } from './create_mock_levellogger'; export const createMockPluginSetup = ( setupMock: Partial> @@ -38,13 +42,13 @@ export const createMockPluginSetup = ( router: { get: jest.fn(), post: jest.fn(), put: jest.fn(), delete: jest.fn() }, security: securityMock.createSetup(), taskManager: taskManagerMock.createSetup(), - logger: createMockLevelLogger(), + logger: loggingSystemMock.createLogger(), status: statusServiceMock.createSetupContract(), ...setupMock, }; }; -const logger = createMockLevelLogger(); +const logger = loggingSystemMock.createLogger(); const createMockReportingStore = async (config: ReportingConfigType) => { const mockConfigSchema = createMockConfigSchema(config); diff --git a/x-pack/plugins/reporting/server/test_helpers/index.ts b/x-pack/plugins/reporting/server/test_helpers/index.ts index df0a182075341a..0e1dffe142c748 100644 --- a/x-pack/plugins/reporting/server/test_helpers/index.ts +++ b/x-pack/plugins/reporting/server/test_helpers/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -export { createMockLevelLogger } from './create_mock_levellogger'; export { createMockConfig, createMockConfigSchema, diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index fa69509d16be8a..b3c9261bfd9244 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { IRouter, RequestHandlerContext } from 'src/core/server'; +import type { IRouter, Logger, RequestHandlerContext } from 'kibana/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import type { DataPluginStart } from 'src/plugins/data/server/plugin'; import { FieldFormatsStart } from 'src/plugins/field_formats/server'; @@ -29,7 +29,6 @@ import type { CancellationToken } from '../common/cancellation_token'; import type { BaseParams, BasePayload, TaskRunResult, UrlOrUrlLocatorTuple } from '../common/types'; import type { ReportingConfigType } from './config'; import type { ReportingCore } from './core'; -import type { LevelLogger } from './lib'; import type { ReportTaskParams } from './lib/tasks'; /** @@ -71,12 +70,12 @@ export type RunTaskFn = ( export type CreateJobFnFactory = ( reporting: ReportingCore, - logger: LevelLogger + logger: Logger ) => CreateJobFnType; export type RunTaskFnFactory = ( reporting: ReportingCore, - logger: LevelLogger + logger: Logger ) => RunTaskFnType; export interface ExportTypeDefinition< From a79562a67e1dcffe18b5da7c08dde57a1374acef Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Mon, 7 Mar 2022 15:14:41 +0100 Subject: [PATCH 11/19] [SecuritySolution] Alerts table Fields Browser revamp (#126105) * field browser first revamp implementation * customize columns for security solution alert tables * cleaning * some tests * clean unused code * field browser tests created and existing fixed * security solution test fixes * translations cleaned * fix test * adapt cypress tests * remove translation * fix typo * remove duplicated test * type error fixed * enable body vertical scroll for small screens * fix new field not added to the table bug * addapt Kevin performance improvement * fixed linter error Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../integration/hosts/events_viewer.spec.ts | 14 +- .../timelines/fields_browser.spec.ts | 116 ++--- .../cypress/screens/fields_browser.ts | 26 +- .../cypress/tasks/fields_browser.ts | 31 +- .../components/events_viewer/index.test.tsx | 2 +- .../common/components/events_viewer/index.tsx | 12 +- .../truncatable_text/truncatable_text.tsx | 2 +- .../components/alerts_table/index.tsx | 3 +- .../detection_engine/detection_engine.tsx | 42 +- .../create_field_button/index.test.tsx | 12 +- .../create_field_button/index.tsx | 29 +- .../create_field_button/translations.ts | 0 .../components/fields_browser/field_items.tsx | 143 ------ .../fields_browser/field_name.test.tsx | 81 ---- .../components/fields_browser/field_name.tsx | 165 ------- .../field_table_columns/index.tsx | 117 +++++ .../field_table_columns/translations.ts | 36 ++ .../components/fields_browser/index.tsx | 31 ++ .../timeline/body/actions/header_actions.tsx | 4 +- .../body/column_headers/index.test.tsx | 2 +- .../timeline/body/column_headers/index.tsx | 20 +- .../components/timeline/body/index.test.tsx | 2 +- x-pack/plugins/timelines/common/index.ts | 2 +- .../search_strategy/index_fields/index.ts | 2 + .../common/types/fields_browser/index.ts | 50 +++ .../plugins/timelines/common/types/index.ts | 1 + .../common/types/timeline/actions/index.ts | 5 +- .../timelines/common/types/timeline/index.ts | 4 - .../components/fields_browser/index.tsx | 9 +- .../public/components/t_grid/body/index.tsx | 22 +- .../components/t_grid/integrated/index.tsx | 8 +- .../fields_browser/categories_badges.test.tsx | 60 +++ .../fields_browser/categories_badges.tsx | 56 +++ .../fields_browser/categories_pane.test.tsx | 51 --- .../fields_browser/categories_pane.tsx | 118 ----- .../categories_selector.test.tsx | 92 ++++ .../fields_browser/categories_selector.tsx | 173 ++++++++ .../toolbar/fields_browser/category.test.tsx | 100 ----- .../toolbar/fields_browser/category.tsx | 114 ----- .../fields_browser/category_columns.test.tsx | 153 ------- .../fields_browser/category_columns.tsx | 157 ------- .../fields_browser/category_title.test.tsx | 72 --- .../toolbar/fields_browser/category_title.tsx | 67 --- .../fields_browser/field_browser.test.tsx | 49 +-- .../toolbar/fields_browser/field_browser.tsx | 145 ++---- .../fields_browser/field_items.test.tsx | 416 +++++++----------- .../toolbar/fields_browser/field_items.tsx | 230 ++++++---- .../fields_browser/field_name.test.tsx | 2 +- .../toolbar/fields_browser/field_name.tsx | 2 +- .../fields_browser/field_table.test.tsx | 225 ++++++++++ .../toolbar/fields_browser/field_table.tsx | 126 ++++++ .../fields_browser/fields_pane.test.tsx | 112 ----- .../toolbar/fields_browser/fields_pane.tsx | 145 ------ .../toolbar/fields_browser/helpers.test.tsx | 33 -- .../t_grid/toolbar/fields_browser/helpers.tsx | 311 +------------ .../toolbar/fields_browser/index.test.tsx | 138 +++--- .../t_grid/toolbar/fields_browser/index.tsx | 108 ++--- .../toolbar/fields_browser/search.test.tsx | 74 +--- .../t_grid/toolbar/fields_browser/search.tsx | 69 +-- .../toolbar/fields_browser/translations.ts | 33 +- .../t_grid/toolbar/fields_browser/types.ts | 27 -- .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - 63 files changed, 1670 insertions(+), 2787 deletions(-) rename x-pack/plugins/security_solution/public/timelines/components/{ => fields_browser}/create_field_button/index.test.tsx (92%) rename x-pack/plugins/security_solution/public/timelines/components/{ => fields_browser}/create_field_button/index.tsx (80%) rename x-pack/plugins/security_solution/public/timelines/components/{ => fields_browser}/create_field_button/translations.ts (100%) delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/index.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/translations.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx create mode 100644 x-pack/plugins/timelines/common/types/fields_browser/index.ts create mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_badges.test.tsx create mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_badges.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.test.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.tsx create mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_selector.test.tsx create mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_selector.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.test.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.test.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.test.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.tsx create mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.test.tsx create mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.test.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/types.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts b/x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts index c28c55e0eb3f7f..47e71345ff0c49 100644 --- a/x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts @@ -8,7 +8,7 @@ import { FIELDS_BROWSER_CHECKBOX, FIELDS_BROWSER_CONTAINER, - FIELDS_BROWSER_SELECTED_CATEGORY_TITLE, + FIELDS_BROWSER_SELECTED_CATEGORIES_BADGES, } from '../../screens/fields_browser'; import { HOST_GEO_CITY_NAME_HEADER, @@ -17,7 +17,11 @@ import { SERVER_SIDE_EVENT_COUNT, } from '../../screens/hosts/events'; -import { closeFieldsBrowser, filterFieldsBrowser } from '../../tasks/fields_browser'; +import { + closeFieldsBrowser, + filterFieldsBrowser, + toggleCategory, +} from '../../tasks/fields_browser'; import { loginAndWaitForPage } from '../../tasks/login'; import { openEvents } from '../../tasks/hosts/main'; import { @@ -60,11 +64,13 @@ describe('Events Viewer', () => { cy.get(FIELDS_BROWSER_CONTAINER).should('not.exist'); }); - it('displays the `default ECS` category (by default)', () => { - cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_TITLE).should('have.text', 'default ECS'); + it('displays all categories (by default)', () => { + cy.get(FIELDS_BROWSER_SELECTED_CATEGORIES_BADGES).should('be.empty'); }); it('displays a checked checkbox for all of the default events viewer columns that are also in the default ECS category', () => { + const category = 'default ECS'; + toggleCategory(category); defaultHeadersInDefaultEcsCategory.forEach((header) => cy.get(FIELDS_BROWSER_CHECKBOX(header.id)).should('be.checked') ); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts index 07ea4078ce7c4b..89a9fc4c0c6ba1 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts @@ -8,14 +8,13 @@ import { FIELDS_BROWSER_CATEGORIES_COUNT, FIELDS_BROWSER_FIELDS_COUNT, - FIELDS_BROWSER_HOST_CATEGORIES_COUNT, FIELDS_BROWSER_HOST_GEO_CITY_NAME_HEADER, FIELDS_BROWSER_HEADER_HOST_GEO_CONTINENT_NAME_HEADER, FIELDS_BROWSER_MESSAGE_HEADER, - FIELDS_BROWSER_SELECTED_CATEGORY_TITLE, - FIELDS_BROWSER_SELECTED_CATEGORY_COUNT, - FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT, FIELDS_BROWSER_FILTER_INPUT, + FIELDS_BROWSER_CATEGORIES_FILTER_CONTAINER, + FIELDS_BROWSER_SELECTED_CATEGORIES_BADGES, + FIELDS_BROWSER_CATEGORY_BADGE, } from '../../screens/fields_browser'; import { TIMELINE_FIELDS_BUTTON } from '../../screens/timeline'; import { cleanKibana } from '../../tasks/common'; @@ -26,13 +25,14 @@ import { clearFieldsBrowser, closeFieldsBrowser, filterFieldsBrowser, + toggleCategoryFilter, removesMessageField, resetFields, + toggleCategory, } from '../../tasks/fields_browser'; import { loginAndWaitForPage } from '../../tasks/login'; import { openTimelineUsingToggle } from '../../tasks/security_main'; import { openTimelineFieldsBrowser, populateTimeline } from '../../tasks/timeline'; -import { ecsFieldMap } from '../../../../rule_registry/common/assets/field_maps/ecs_field_map'; import { HOSTS_URL } from '../../urls/navigation'; @@ -61,21 +61,8 @@ describe('Fields Browser', () => { clearFieldsBrowser(); }); - it('displays the `default ECS` category (by default)', () => { - cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_TITLE).should('have.text', 'default ECS'); - }); - - it('the `defaultECS` (selected) category count matches the default timeline header count', () => { - cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_COUNT).should( - 'have.text', - `${defaultHeaders.length}` - ); - }); - - it('displays a checked checkbox for all of the default timeline columns', () => { - defaultHeaders.forEach((header) => - cy.get(`[data-test-subj="field-${header.id}-checkbox"]`).should('be.checked') - ); + it('displays all categories (by default)', () => { + cy.get(FIELDS_BROWSER_SELECTED_CATEGORIES_BADGES).should('be.empty'); }); it('displays the expected count of categories that match the filter input', () => { @@ -83,54 +70,50 @@ describe('Fields Browser', () => { filterFieldsBrowser(filterInput); - cy.get(FIELDS_BROWSER_CATEGORIES_COUNT).should('have.text', '2 categories'); + cy.get(FIELDS_BROWSER_CATEGORIES_COUNT).should('have.text', '2'); }); it('displays a search results label with the expected count of fields matching the filter input', () => { const filterInput = 'host.mac'; - filterFieldsBrowser(filterInput); - cy.get(FIELDS_BROWSER_HOST_CATEGORIES_COUNT) - .invoke('text') - .then((hostCategoriesCount) => { - cy.get(FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT) - .invoke('text') - .then((systemCategoriesCount) => { - cy.get(FIELDS_BROWSER_FIELDS_COUNT).should( - 'have.text', - `${+hostCategoriesCount + +systemCategoriesCount} fields` - ); - }); - }); - }); - - it('displays a count of only the fields in the selected category that match the filter input', () => { - const filterInput = 'host.geo.c'; + cy.get(FIELDS_BROWSER_FIELDS_COUNT).should('contain.text', '2'); + }); - filterFieldsBrowser(filterInput); + it('the `default ECS` category matches the default timeline header fields', () => { + const category = 'default ECS'; + toggleCategory(category); + cy.get(FIELDS_BROWSER_FIELDS_COUNT).should('contain.text', `${defaultHeaders.length}`); + + defaultHeaders.forEach((header) => { + cy.get(`[data-test-subj="field-${header.id}-checkbox"]`).should('be.checked'); + }); + toggleCategory(category); + }); + + it('creates the category badge when it is selected', () => { + const category = 'host'; + + cy.get(FIELDS_BROWSER_CATEGORY_BADGE(category)).should('not.exist'); + toggleCategory(category); + cy.get(FIELDS_BROWSER_CATEGORY_BADGE(category)).should('exist'); + toggleCategory(category); + }); + + it('search a category should match the category in the category filter', () => { + const category = 'host'; - const fieldsThatMatchFilterInput = Object.keys(ecsFieldMap).filter((fieldName) => { - const dotDelimitedFieldParts = fieldName.split('.'); - const fieldPartMatch = dotDelimitedFieldParts.filter((fieldPart) => { - const camelCasedStringsMatching = fieldPart - .split('_') - .some((part) => part.startsWith(filterInput)); - if (fieldPart.startsWith(filterInput)) { - return true; - } else if (camelCasedStringsMatching) { - return true; - } else { - return false; - } - }); - return fieldName.startsWith(filterInput) || fieldPartMatch.length > 0; - }).length; - - cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_COUNT).should( - 'have.text', - fieldsThatMatchFilterInput - ); + filterFieldsBrowser(category); + toggleCategoryFilter(); + cy.get(FIELDS_BROWSER_CATEGORIES_FILTER_CONTAINER).should('contain.text', category); + }); + + it('search a category should filter out non matching categories in the category filter', () => { + const category = 'host'; + const categoryCheck = 'event'; + filterFieldsBrowser(category); + toggleCategoryFilter(); + cy.get(FIELDS_BROWSER_CATEGORIES_FILTER_CONTAINER).should('not.contain.text', categoryCheck); }); }); @@ -157,18 +140,15 @@ describe('Fields Browser', () => { cy.get(FIELDS_BROWSER_MESSAGE_HEADER).should('not.exist'); }); - it('selects a search results label with the expected count of categories matching the filter input', () => { - const category = 'host'; - filterFieldsBrowser(category); - - cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_TITLE).should('have.text', category); - }); - it('adds a field to the timeline when the user clicks the checkbox', () => { const filterInput = 'host.geo.c'; - filterFieldsBrowser(filterInput); + closeFieldsBrowser(); cy.get(FIELDS_BROWSER_HOST_GEO_CITY_NAME_HEADER).should('not.exist'); + + openTimelineFieldsBrowser(); + + filterFieldsBrowser(filterInput); addsHostGeoCityNameToTimeline(); closeFieldsBrowser(); diff --git a/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts b/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts index 4a5f813c301db0..66a7ba50c8070e 100644 --- a/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts +++ b/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts @@ -7,20 +7,16 @@ export const CLOSE_BTN = '[data-test-subj="close"]'; -export const FIELDS_BROWSER_CATEGORIES_COUNT = '[data-test-subj="categories-count"]'; +export const FIELDS_BROWSER_CONTAINER = '[data-test-subj="fields-browser-container"]'; export const FIELDS_BROWSER_CHECKBOX = (id: string) => { - return `[data-test-subj="category-table-container"] [data-test-subj="field-${id}-checkbox"]`; + return `${FIELDS_BROWSER_CONTAINER} [data-test-subj="field-${id}-checkbox"]`; }; -export const FIELDS_BROWSER_CONTAINER = '[data-test-subj="fields-browser-container"]'; - export const FIELDS_BROWSER_FIELDS_COUNT = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="fields-count"]`; export const FIELDS_BROWSER_FILTER_INPUT = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="field-search"]`; -export const FIELDS_BROWSER_HOST_CATEGORIES_COUNT = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="host-category-count"]`; - export const FIELDS_BROWSER_HOST_GEO_CITY_NAME_CHECKBOX = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="field-host.geo.city_name-checkbox"]`; export const FIELDS_BROWSER_HOST_GEO_CITY_NAME_HEADER = @@ -38,8 +34,22 @@ export const FIELDS_BROWSER_MESSAGE_HEADER = export const FIELDS_BROWSER_RESET_FIELDS = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="reset-fields"]`; -export const FIELDS_BROWSER_SELECTED_CATEGORY_COUNT = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="selected-category-count-badge"]`; +export const FIELDS_BROWSER_CATEGORIES_FILTER_BUTTON = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="categories-filter-button"]`; +export const FIELDS_BROWSER_SELECTED_CATEGORY_COUNT = `${FIELDS_BROWSER_CATEGORIES_FILTER_BUTTON} span.euiNotificationBadge`; +export const FIELDS_BROWSER_CATEGORIES_COUNT = `${FIELDS_BROWSER_CATEGORIES_FILTER_BUTTON} span.euiNotificationBadge`; -export const FIELDS_BROWSER_SELECTED_CATEGORY_TITLE = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="selected-category-title"]`; +export const FIELDS_BROWSER_SELECTED_CATEGORIES_BADGES = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="category-badges"]`; +export const FIELDS_BROWSER_CATEGORY_BADGE = (id: string) => { + return `${FIELDS_BROWSER_SELECTED_CATEGORIES_BADGES} [data-test-subj="category-badge-${id}"]`; +}; + +export const FIELDS_BROWSER_CATEGORIES_FILTER_CONTAINER = + '[data-test-subj="categories-selector-container"]'; +export const FIELDS_BROWSER_CATEGORIES_FILTER_SEARCH = + '[data-test-subj="categories-selector-search"]'; +export const FIELDS_BROWSER_CATEGORY_FILTER_OPTION = (id: string) => { + const idAttr = id.replace(/\s/g, ''); + return `${FIELDS_BROWSER_CATEGORIES_FILTER_CONTAINER} [data-test-subj="categories-selector-option-${idAttr}"]`; +}; export const FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="system-category-count"]`; diff --git a/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts b/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts index 941a19669f2efb..04b59305b591a2 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts @@ -13,6 +13,9 @@ import { FIELDS_BROWSER_RESET_FIELDS, FIELDS_BROWSER_CHECKBOX, CLOSE_BTN, + FIELDS_BROWSER_CATEGORIES_FILTER_BUTTON, + FIELDS_BROWSER_CATEGORY_FILTER_OPTION, + FIELDS_BROWSER_CATEGORIES_FILTER_SEARCH, } from '../screens/fields_browser'; export const addsFields = (fields: string[]) => { @@ -34,10 +37,9 @@ export const addsHostGeoContinentNameToTimeline = () => { }; export const clearFieldsBrowser = () => { - cy.clock(); - cy.get(FIELDS_BROWSER_FILTER_INPUT).type('{selectall}{backspace}'); - cy.wait(0); - cy.tick(1000); + cy.get(FIELDS_BROWSER_FILTER_INPUT) + .type('{selectall}{backspace}') + .waitUntil((subject) => !subject.hasClass('euiFieldSearch-isLoading')); }; export const closeFieldsBrowser = () => { @@ -46,12 +48,21 @@ export const closeFieldsBrowser = () => { }; export const filterFieldsBrowser = (fieldName: string) => { - cy.clock(); - cy.get(FIELDS_BROWSER_FILTER_INPUT).type(fieldName, { delay: 50 }); - cy.wait(0); - cy.tick(1000); - // the text filter is debounced by 250 ms, wait 1s for changes to be applied - cy.get(FIELDS_BROWSER_FILTER_INPUT).should('not.have.class', 'euiFieldSearch-isLoading'); + cy.get(FIELDS_BROWSER_FILTER_INPUT) + .clear() + .type(fieldName) + .waitUntil((subject) => !subject.hasClass('euiFieldSearch-isLoading')); +}; + +export const toggleCategoryFilter = () => { + cy.get(FIELDS_BROWSER_CATEGORIES_FILTER_BUTTON).click({ force: true }); +}; + +export const toggleCategory = (category: string) => { + toggleCategoryFilter(); + cy.get(FIELDS_BROWSER_CATEGORIES_FILTER_SEARCH).clear().type(category); + cy.get(FIELDS_BROWSER_CATEGORY_FILTER_OPTION(category)).click({ force: true }); + toggleCategoryFilter(); }; export const removesMessageField = () => { diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx index 2ecae444879082..cdc9cc9b6f32dc 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx @@ -34,7 +34,7 @@ jest.mock('../../../timelines/containers', () => ({ jest.mock('../../components/url_state/normalize_time_range.ts'); const mockUseCreateFieldButton = jest.fn().mockReturnValue(<>); -jest.mock('../../../timelines/components/create_field_button', () => ({ +jest.mock('../../../timelines/components/fields_browser/create_field_button', () => ({ useCreateFieldButton: (...params: unknown[]) => mockUseCreateFieldButton(...params), })); 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 5e3fc4e81f9dc3..68c4af5ee2fe8f 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 @@ -30,9 +30,9 @@ import { FIELDS_WITHOUT_CELL_ACTIONS } from '../../lib/cell_actions/constants'; import { useGetUserCasesPermissions, useKibana } from '../../lib/kibana'; import { GraphOverlay } from '../../../timelines/components/graph_overlay'; import { + useFieldBrowserOptions, CreateFieldEditorActions, - useCreateFieldButton, -} from '../../../timelines/components/create_field_button'; +} from '../../../timelines/components/fields_browser'; const EMPTY_CONTROL_COLUMNS: ControlColumnProps[] = []; @@ -177,7 +177,11 @@ const StatefulEventsViewerComponent: React.FC = ({ }, [id, timelineQuery, globalQuery]); const bulkActions = useMemo(() => ({ onAlertStatusActionSuccess }), [onAlertStatusActionSuccess]); - const createFieldComponent = useCreateFieldButton(scopeId, id, editorActionsRef); + const fieldBrowserOptions = useFieldBrowserOptions({ + sourcererScope: scopeId, + timelineId: id, + editorActionsRef, + }); const casesPermissions = useGetUserCasesPermissions(); const CasesContext = casesUi.getCasesContext(); @@ -201,6 +205,7 @@ const StatefulEventsViewerComponent: React.FC = ({ docValueFields, end, entityType, + fieldBrowserOptions, filters: globalFilters, filterStatus: currentFilter, globalFullScreen, @@ -228,7 +233,6 @@ const StatefulEventsViewerComponent: React.FC = ({ trailingControlColumns, type: 'embedded', unit, - createFieldComponent, })} diff --git a/x-pack/plugins/security_solution/public/common/components/truncatable_text/truncatable_text.tsx b/x-pack/plugins/security_solution/public/common/components/truncatable_text/truncatable_text.tsx index cc1c53d1071002..27369dadb8a3bf 100644 --- a/x-pack/plugins/security_solution/public/common/components/truncatable_text/truncatable_text.tsx +++ b/x-pack/plugins/security_solution/public/common/components/truncatable_text/truncatable_text.tsx @@ -16,7 +16,7 @@ import { EuiToolTip } from '@elastic/eui'; * Note: Requires a parent container with a defined width or max-width. */ -const EllipsisText = styled.span` +export const EllipsisText = styled.span` &, & * { display: inline-block; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 1499e803fdf37e..0f6d2d260ae0d7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -99,7 +99,6 @@ export const AlertsTableComponent: React.FC = ({ const { browserFields, indexPattern: indexPatterns, - loading: indexPatternsLoading, selectedPatterns, } = useSourcererDataView(SourcererScopeName.detections); const kibana = useKibana(); @@ -360,7 +359,7 @@ export const AlertsTableComponent: React.FC = ({ const casesPermissions = useGetUserCasesPermissions(); const CasesContext = kibana.services.cases.getCasesContext(); - if (loading || indexPatternsLoading || isEmpty(selectedPatterns)) { + if (loading || isEmpty(selectedPatterns)) { return null; } diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index e4f51b05ad6d9b..eccb2e081cd9de 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -140,7 +140,7 @@ const DetectionEnginePageComponent: React.FC = ({ const { formatUrl } = useFormatUrl(SecurityPageName.rules); const [showBuildingBlockAlerts, setShowBuildingBlockAlerts] = useState(false); const [showOnlyThreatIndicatorAlerts, setShowOnlyThreatIndicatorAlerts] = useState(false); - const loading = userInfoLoading || listsConfigLoading || isLoadingIndexPattern; + const loading = userInfoLoading || listsConfigLoading; const { application: { navigateToUrl }, timelines: timelinesUi, @@ -341,24 +341,32 @@ const DetectionEnginePageComponent: React.FC = ({ - + {isLoadingIndexPattern ? ( + + ) : ( + + )} - + {isLoadingIndexPattern ? ( + + ) : ( + + )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/create_field_button/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.test.tsx similarity index 92% rename from x-pack/plugins/security_solution/public/timelines/components/create_field_button/index.test.tsx rename to x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.test.tsx index 0afb2bf6413517..1bddd96c057277 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/create_field_button/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.test.tsx @@ -11,15 +11,15 @@ import { CreateFieldButton, CreateFieldEditorActions } from './index'; import { indexPatternFieldEditorPluginMock, Start, -} from '../../../../../../../src/plugins/data_view_field_editor/public/mocks'; +} from '../../../../../../../../src/plugins/data_view_field_editor/public/mocks'; -import { TestProviders } from '../../../common/mock'; -import { useKibana } from '../../../common/lib/kibana'; -import type { DataView } from '../../../../../../../src/plugins/data/common'; -import { TimelineId } from '../../../../common/types'; +import { TestProviders } from '../../../../common/mock'; +import { useKibana } from '../../../../common/lib/kibana'; +import type { DataView } from '../../../../../../../../src/plugins/data/common'; +import { TimelineId } from '../../../../../common/types'; let mockIndexPatternFieldEditor: Start; -jest.mock('../../../common/lib/kibana'); +jest.mock('../../../../common/lib/kibana'); const useKibanaMock = useKibana as jest.Mocked; const runAllPromises = () => new Promise(setImmediate); diff --git a/x-pack/plugins/security_solution/public/timelines/components/create_field_button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.tsx similarity index 80% rename from x-pack/plugins/security_solution/public/timelines/components/create_field_button/index.tsx rename to x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.tsx index 8979a78d7aa465..645e1f0b29aed5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/create_field_button/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.tsx @@ -10,23 +10,26 @@ import { EuiButton } from '@elastic/eui'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; -import type { DataViewField, DataView } from '../../../../../../../src/plugins/data_views/common'; -import { useKibana } from '../../../common/lib/kibana'; +import type { + DataViewField, + DataView, +} from '../../../../../../../../src/plugins/data_views/common'; +import { useKibana } from '../../../../common/lib/kibana'; import * as i18n from './translations'; -import { CreateFieldComponentType, TimelineId } from '../../../../../timelines/common'; -import { upsertColumn } from '../../../../../timelines/public'; -import { useDataView } from '../../../common/containers/source/use_data_view'; -import { SourcererScopeName } from '../../../common/store/sourcerer/model'; -import { sourcererSelectors } from '../../../common/store'; -import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; -import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/constants'; -import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; +import { FieldBrowserOptions, TimelineId } from '../../../../../../timelines/common'; +import { upsertColumn } from '../../../../../../timelines/public'; +import { useDataView } from '../../../../common/containers/source/use_data_view'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { sourcererSelectors } from '../../../../common/store'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { DEFAULT_COLUMN_MIN_WIDTH } from '../../timeline/body/constants'; +import { defaultColumnHeaderType } from '../../timeline/body/column_headers/default_headers'; export type CreateFieldEditorActions = { closeEditor: () => void } | null; -type CreateFieldEditorActionsRef = MutableRefObject; +export type CreateFieldEditorActionsRef = MutableRefObject; -interface CreateFieldButtonProps { +export interface CreateFieldButtonProps { selectedDataViewId: string; onClick: () => void; timelineId: TimelineId; @@ -142,7 +145,7 @@ export const useCreateFieldButton = ( return; } // It receives onClick props from field browser in order to close the modal. - const CreateFieldButtonComponent: CreateFieldComponentType = ({ onClick }) => ( + const CreateFieldButtonComponent: FieldBrowserOptions['createFieldButton'] = ({ onClick }) => ( void; -}) => { - const keyboardHandlerRef = useRef(null); - const [closePopOverTrigger, setClosePopOverTrigger] = useState(false); - const [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState(false); - const { timelines } = useKibana().services; - - const handleClosePopOverTrigger = useCallback(() => { - setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger); - - setHoverActionsOwnFocus((prevHoverActionsOwnFocus) => { - if (prevHoverActionsOwnFocus) { - // on the next tick, re-focus the keyboard handler if the hover actions owned focus - setTimeout(() => { - keyboardHandlerRef.current?.focus(); - }, 0); - } - return false; // always give up ownership - }); - - setTimeout(() => { - setHoverActionsOwnFocus(false); - }, 0); // invoked on the next tick, because we want to restore focus first - }, []); - - const openPopover = useCallback(() => { - setHoverActionsOwnFocus(true); - }, [setHoverActionsOwnFocus]); - - const { onBlur, onKeyDown } = timelines.getUseDraggableKeyboardWrapper()({ - closePopover: handleClosePopOverTrigger, - draggableId: getDraggableFieldId({ - contextId: `field-browser-field-items-field-draggable-${timelineId}-${categoryId}-${fieldName}`, - fieldId: fieldName, - }), - fieldName, - keyboardHandlerRef, - openPopover, - }); - - const onFocus = useCallback(() => { - keyboardHandlerRef.current?.focus(); - }, []); - - const onCloseRequested = useCallback(() => { - setHoverActionsOwnFocus((prevHoverActionOwnFocus) => - prevHoverActionOwnFocus ? false : prevHoverActionOwnFocus - ); - - setTimeout(() => { - onFocus(); // return focus to this draggable on the next tick, because we owned focus - }, 0); - }, [onFocus]); - - return ( -
- - {(provided) => ( -
- -
- )} -
-
- ); -}; - -export const DraggableFieldsBrowserField = React.memo(DraggableFieldsBrowserFieldComponent); -DraggableFieldsBrowserField.displayName = 'DraggableFieldsBrowserFieldComponent'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx deleted file mode 100644 index 5acc0ef9aa46b3..00000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mount } from 'enzyme'; -import React from 'react'; -import { waitFor } from '@testing-library/react'; -import { mockBrowserFields } from '../../../common/containers/source/mock'; -import { TestProviders } from '../../../common/mock'; -import '../../../common/mock/match_media'; -import { getColumnsWithTimestamp } from '../../../common/components/event_details/helpers'; - -import { FieldName } from './field_name'; - -jest.mock('../../../common/lib/kibana'); - -const categoryId = 'base'; -const timestampFieldId = '@timestamp'; - -const defaultProps = { - categoryId, - categoryColumns: getColumnsWithTimestamp({ - browserFields: mockBrowserFields, - category: categoryId, - }), - closePopOverTrigger: false, - fieldId: timestampFieldId, - handleClosePopOverTrigger: jest.fn(), - hoverActionsOwnFocus: false, - onCloseRequested: jest.fn(), - onUpdateColumns: jest.fn(), - setClosePopOverTrigger: jest.fn(), -}; - -describe('FieldName', () => { - beforeEach(() => { - jest.useFakeTimers(); - }); - - test('it renders the field name', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`[data-test-subj="field-name-${timestampFieldId}"]`).first().text() - ).toEqual(timestampFieldId); - }); - - test('it renders a copy to clipboard action menu item a user hovers over the name', async () => { - const wrapper = mount( - - - - ); - await waitFor(() => { - wrapper.find('[data-test-subj="withHoverActionsButton"]').simulate('mouseenter'); - wrapper.update(); - jest.runAllTimers(); - wrapper.update(); - expect(wrapper.find('[data-test-subj="hover-actions-copy-button"]').exists()).toBe(true); - }); - }); - - test('it highlights the text specified by the `highlight` prop', () => { - const highlight = 'stamp'; - - const wrapper = mount( - - - - ); - - expect(wrapper.find('mark').first().text()).toEqual(highlight); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx deleted file mode 100644 index 6e9672d08b3666..00000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiHighlight, EuiText } from '@elastic/eui'; -import React, { useCallback, useState, useMemo, useRef, useContext } from 'react'; -import styled from 'styled-components'; - -import { OnUpdateColumns } from '../timeline/events'; -import { WithHoverActions } from '../../../common/components/with_hover_actions'; -import { ColumnHeaderOptions } from '../../../../common/types'; -import { HoverActions } from '../../../common/components/hover_actions'; -import { TimelineContext } from '../../../../../timelines/public'; - -/** - * The name of a (draggable) field - */ -export const FieldNameContainer = styled.span` - border-radius: 4px; - display: flex; - padding: 0 4px 0 8px; - position: relative; - - &::before { - background-image: linear-gradient( - 135deg, - ${({ theme }) => theme.eui.euiColorMediumShade} 25%, - transparent 25% - ), - linear-gradient(-135deg, ${({ theme }) => theme.eui.euiColorMediumShade} 25%, transparent 25%), - linear-gradient(135deg, transparent 75%, ${({ theme }) => theme.eui.euiColorMediumShade} 75%), - linear-gradient(-135deg, transparent 75%, ${({ theme }) => theme.eui.euiColorMediumShade} 75%); - background-position: 0 0, 1px 0, 1px -1px, 0px 1px; - background-size: 2px 2px; - bottom: 2px; - content: ''; - display: block; - left: 2px; - position: absolute; - top: 2px; - width: 4px; - } - - &:hover, - &:focus { - transition: background-color 0.7s ease; - background-color: #000; - color: #fff; - - &::before { - background-image: linear-gradient(135deg, #fff 25%, transparent 25%), - linear-gradient( - -135deg, - ${({ theme }) => theme.eui.euiColorLightestShade} 25%, - transparent 25% - ), - linear-gradient( - 135deg, - transparent 75%, - ${({ theme }) => theme.eui.euiColorLightestShade} 75% - ), - linear-gradient( - -135deg, - transparent 75%, - ${({ theme }) => theme.eui.euiColorLightestShade} 75% - ); - } - } -`; - -FieldNameContainer.displayName = 'FieldNameContainer'; - -/** Renders a field name in it's non-dragging state */ -export const FieldName = React.memo<{ - categoryId: string; - categoryColumns: ColumnHeaderOptions[]; - closePopOverTrigger: boolean; - fieldId: string; - highlight?: string; - handleClosePopOverTrigger: () => void; - hoverActionsOwnFocus: boolean; - onCloseRequested: () => void; - onUpdateColumns: OnUpdateColumns; -}>( - ({ - closePopOverTrigger, - fieldId, - highlight = '', - handleClosePopOverTrigger, - hoverActionsOwnFocus, - onCloseRequested, - }) => { - const containerRef = useRef(null); - const [showTopN, setShowTopN] = useState(false); - const { timelineId: timelineIdFind } = useContext(TimelineContext); - - const toggleTopN = useCallback(() => { - setShowTopN((prevShowTopN) => { - const newShowTopN = !prevShowTopN; - if (newShowTopN === false) { - handleClosePopOverTrigger(); - } - return newShowTopN; - }); - }, [handleClosePopOverTrigger]); - - const closeTopN = useCallback(() => { - setShowTopN(false); - }, []); - - const hoverContent = useMemo( - () => ( - - ), - [ - closeTopN, - fieldId, - handleClosePopOverTrigger, - hoverActionsOwnFocus, - showTopN, - timelineIdFind, - toggleTopN, - ] - ); - - const render = useCallback( - () => ( - - - - {fieldId} - - - - ), - [fieldId, highlight] - ); - - return ( -
- -
- ); - } -); - -FieldName.displayName = 'FieldName'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/index.tsx new file mode 100644 index 00000000000000..b060575fdc5cb5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/index.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import styled from 'styled-components'; +import { + EuiToolTip, + EuiFlexGroup, + EuiFlexItem, + EuiScreenReaderOnly, + EuiHealth, + EuiBadge, + EuiIcon, + EuiText, + EuiHighlight, +} from '@elastic/eui'; +import type { FieldTableColumns } from '../../../../../../timelines/common/types'; +import * as i18n from './translations'; +import { + getExampleText, + getIconFromType, +} from '../../../../common/components/event_details/helpers'; +import { getEmptyValue } from '../../../../common/components/empty_value'; +import { EllipsisText } from '../../../../common/components/truncatable_text'; + +const TypeIcon = styled(EuiIcon)` + margin: 0 4px; + position: relative; + top: -1px; +`; +TypeIcon.displayName = 'TypeIcon'; + +export const Description = styled.span` + user-select: text; + width: 400px; +`; +Description.displayName = 'Description'; + +export const FieldName = React.memo<{ + fieldId: string; + highlight?: string; +}>(({ fieldId, highlight = '' }) => ( + + + {fieldId} + + +)); +FieldName.displayName = 'FieldName'; + +export const getFieldTableColumns = (highlight: string): FieldTableColumns => [ + { + field: 'name', + name: i18n.NAME, + render: (name: string, { type }) => { + return ( + + + + + + + + + + + + ); + }, + sortable: true, + width: '200px', + }, + { + field: 'description', + name: i18n.DESCRIPTION, + render: (description, { name, example }) => ( + + <> + +

{i18n.DESCRIPTION_FOR_FIELD(name)}

+
+ + + {`${description ?? getEmptyValue()} ${getExampleText(example)}`} + + + +
+ ), + sortable: true, + width: '400px', + }, + { + field: 'isRuntime', + name: i18n.RUNTIME, + render: (isRuntime: boolean) => + isRuntime ? : null, + sortable: true, + width: '80px', + }, + { + field: 'category', + name: i18n.CATEGORY, + render: (category: string, { name }) => ( + {category} + ), + sortable: true, + width: '100px', + }, +]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/translations.ts new file mode 100644 index 00000000000000..c16307250c2c81 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/translations.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const NAME = i18n.translate('xpack.securitySolution.fieldBrowser.fieldName', { + defaultMessage: 'Name', +}); + +export const DESCRIPTION = i18n.translate('xpack.securitySolution.fieldBrowser.descriptionLabel', { + defaultMessage: 'Description', +}); + +export const DESCRIPTION_FOR_FIELD = (field: string) => + i18n.translate('xpack.securitySolution.fieldBrowser.descriptionForScreenReaderOnly', { + values: { + field, + }, + defaultMessage: 'Description for field {field}:', + }); + +export const CATEGORY = i18n.translate('xpack.securitySolution.fieldBrowser.categoryLabel', { + defaultMessage: 'Category', +}); + +export const RUNTIME = i18n.translate('xpack.securitySolution.fieldBrowser.runtimeLabel', { + defaultMessage: 'Runtime', +}); + +export const RUNTIME_FIELD = i18n.translate('xpack.securitySolution.fieldBrowser.runtimeTitle', { + defaultMessage: 'Runtime Field', +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx new file mode 100644 index 00000000000000..46f2caa147a408 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TimelineId } from '../../../../common/types'; +import { SourcererScopeName } from '../../../common/store/sourcerer/model'; +import { useCreateFieldButton, CreateFieldEditorActionsRef } from './create_field_button'; +import { getFieldTableColumns } from './field_table_columns'; + +export type { CreateFieldEditorActions } from './create_field_button'; + +export interface UseFieldBrowserOptions { + sourcererScope: SourcererScopeName; + timelineId: TimelineId; + editorActionsRef?: CreateFieldEditorActionsRef; +} + +export const useFieldBrowserOptions = ({ + sourcererScope, + timelineId, + editorActionsRef, +}: UseFieldBrowserOptions) => { + const createFieldButton = useCreateFieldButton(sourcererScope, timelineId, editorActionsRef); + return { + createFieldButton, + getFieldTableColumns, + }; +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx index 9636aadbc08e3c..0e26edc6ae1c1a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx @@ -86,7 +86,7 @@ const HeaderActionsComponent: React.FC = ({ sort, tabType, timelineId, - createFieldComponent, + fieldBrowserOptions, }) => { const { timelines: timelinesUi } = useKibana().services; const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); @@ -184,7 +184,7 @@ const HeaderActionsComponent: React.FC = ({ browserFields, columnHeaders, timelineId, - createFieldComponent, + options: fieldBrowserOptions, })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx index aec28732f38afa..7e3de3514f5a78 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx @@ -28,7 +28,7 @@ import { HeaderActions } from '../actions/header_actions'; jest.mock('../../../../../common/lib/kibana'); const mockUseCreateFieldButton = jest.fn().mockReturnValue(<>); -jest.mock('../../../create_field_button', () => ({ +jest.mock('../../../fields_browser/create_field_button', () => ({ useCreateFieldButton: (...params: unknown[]) => mockUseCreateFieldButton(...params), })); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx index ca1cdef903de84..e58dd520181c1a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx @@ -34,7 +34,7 @@ import { Sort } from '../sort'; import { ColumnHeader } from './column_header'; import { SourcererScopeName } from '../../../../../common/store/sourcerer/model'; -import { CreateFieldEditorActions, useCreateFieldButton } from '../../../create_field_button'; +import { useFieldBrowserOptions, CreateFieldEditorActions } from '../../../fields_browser'; export interface ColumnHeadersComponentProps { actionsColumnWidth: number; @@ -190,11 +190,11 @@ export const ColumnHeadersComponent = ({ [trailingControlColumns] ); - const createFieldComponent = useCreateFieldButton( - SourcererScopeName.timeline, - timelineId as TimelineId, - fieldEditorActionsRef - ); + const fieldBrowserOptions = useFieldBrowserOptions({ + sourcererScope: SourcererScopeName.timeline, + timelineId: timelineId as TimelineId, + editorActionsRef: fieldEditorActionsRef, + }); const LeadingHeaderActions = useMemo(() => { return leadingHeaderCells.map( @@ -221,7 +221,7 @@ export const ColumnHeadersComponent = ({ sort={sort} tabType={tabType} timelineId={timelineId} - createFieldComponent={createFieldComponent} + fieldBrowserOptions={fieldBrowserOptions} /> )} @@ -234,7 +234,7 @@ export const ColumnHeadersComponent = ({ actionsColumnWidth, browserFields, columnHeaders, - createFieldComponent, + fieldBrowserOptions, isEventViewer, isSelectAllChecked, onSelectAll, @@ -270,7 +270,7 @@ export const ColumnHeadersComponent = ({ sort={sort} tabType={tabType} timelineId={timelineId} - createFieldComponent={createFieldComponent} + fieldBrowserOptions={fieldBrowserOptions} /> )} @@ -283,7 +283,7 @@ export const ColumnHeadersComponent = ({ actionsColumnWidth, browserFields, columnHeaders, - createFieldComponent, + fieldBrowserOptions, isEventViewer, isSelectAllChecked, onSelectAll, 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 f616b4afc2af5f..5a9f981988d591 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 @@ -114,7 +114,7 @@ jest.mock('../../../../common/lib/helpers/scheduler', () => ({ maxDelay: () => 3000, })); -jest.mock('../../create_field_button', () => ({ +jest.mock('../../fields_browser/create_field_button', () => ({ useCreateFieldButton: () => <>, })); diff --git a/x-pack/plugins/timelines/common/index.ts b/x-pack/plugins/timelines/common/index.ts index 0002dd6eb14327..96728a07432fdb 100644 --- a/x-pack/plugins/timelines/common/index.ts +++ b/x-pack/plugins/timelines/common/index.ts @@ -20,7 +20,6 @@ export type { ActionProps, AlertWorkflowStatus, CellValueElementProps, - CreateFieldComponentType, ColumnId, ColumnRenderer, ColumnHeaderType, @@ -28,6 +27,7 @@ export type { ControlColumnProps, DataProvidersAnd, DataProvider, + FieldBrowserOptions, GenericActionRowCellRenderProps, HeaderActionProps, HeaderCellRender, diff --git a/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts b/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts index 0b83cf28f9bb7a..544ca033b060cc 100644 --- a/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts +++ b/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts @@ -11,6 +11,7 @@ import type { IEsSearchRequest, IEsSearchResponse, FieldSpec, + RuntimeField, } from '../../../../../../src/plugins/data/common'; import type { DocValueFields, Maybe } from '../common'; @@ -71,6 +72,7 @@ export interface BrowserField { type: string; subType?: IFieldSubType; readFromDocValues: boolean; + runtimeField?: RuntimeField; } export type BrowserFields = Readonly>>; diff --git a/x-pack/plugins/timelines/common/types/fields_browser/index.ts b/x-pack/plugins/timelines/common/types/fields_browser/index.ts new file mode 100644 index 00000000000000..7aac02be877d21 --- /dev/null +++ b/x-pack/plugins/timelines/common/types/fields_browser/index.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiBasicTableColumn } from '@elastic/eui'; +import { BrowserFields } from '../../search_strategy'; +import { ColumnHeaderOptions } from '../timeline/columns'; + +/** + * An item rendered in the table + */ +export interface BrowserFieldItem { + name: string; + type?: string; + description?: string; + example?: string; + category: string; + selected: boolean; + isRuntime: boolean; +} + +export type OnFieldSelected = (fieldId: string) => void; + +export type CreateFieldComponent = React.FC<{ + onClick: () => void; +}>; +export type FieldTableColumns = Array>; +export type GetFieldTableColumns = (highlight: string) => FieldTableColumns; +export interface FieldBrowserOptions { + createFieldButton?: CreateFieldComponent; + getFieldTableColumns?: GetFieldTableColumns; +} + +export interface FieldBrowserProps { + /** The timeline associated with this field browser */ + timelineId: string; + /** The timeline's current column headers */ + columnHeaders: ColumnHeaderOptions[]; + /** A map of categoryId -> metadata about the fields in that category */ + browserFields: BrowserFields; + /** When true, this Fields Browser is being used as an "events viewer" */ + isEventViewer?: boolean; + /** The options to customize the field browser, supporting columns rendering and button to create fields */ + options?: FieldBrowserOptions; + /** The width of the field browser */ + width?: number; +} diff --git a/x-pack/plugins/timelines/common/types/index.ts b/x-pack/plugins/timelines/common/types/index.ts index 9464a33082a495..f8e2b030638606 100644 --- a/x-pack/plugins/timelines/common/types/index.ts +++ b/x-pack/plugins/timelines/common/types/index.ts @@ -5,4 +5,5 @@ * 2.0. */ +export * from './fields_browser'; export * from './timeline'; diff --git a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts index 0662c63f35eddb..6a9c6bf8e74a00 100644 --- a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts @@ -7,11 +7,12 @@ import { ComponentType, JSXElementConstructor } from 'react'; import { EuiDataGridControlColumn, EuiDataGridCellValueElementProps } from '@elastic/eui'; -import { CreateFieldComponentType, OnRowSelected, SortColumnTimeline, TimelineTabs } from '..'; +import { OnRowSelected, SortColumnTimeline, TimelineTabs } from '..'; import { BrowserFields } from '../../../search_strategy/index_fields'; import { ColumnHeaderOptions } from '../columns'; import { TimelineNonEcsData } from '../../../search_strategy'; import { Ecs } from '../../../ecs'; +import { FieldBrowserOptions } from '../../fields_browser'; export interface ActionProps { action?: RowCellRender; @@ -67,7 +68,7 @@ export interface HeaderActionProps { width: number; browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; - createFieldComponent?: CreateFieldComponentType; + fieldBrowserOptions?: FieldBrowserOptions; isEventViewer?: boolean; isSelectAllChecked: boolean; onSelectAll: ({ isSelected }: { isSelected: boolean }) => void; diff --git a/x-pack/plugins/timelines/common/types/timeline/index.ts b/x-pack/plugins/timelines/common/types/timeline/index.ts index 4ebc84a41f4b3f..a6c8ed1b74bfff 100644 --- a/x-pack/plugins/timelines/common/types/timeline/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/index.ts @@ -465,10 +465,6 @@ export enum TimelineTabs { eql = 'eql', } -export type CreateFieldComponentType = React.FC<{ - onClick: () => void; -}>; - // eslint-disable-next-line @typescript-eslint/no-explicit-any type EmptyObject = Partial>; diff --git a/x-pack/plugins/timelines/public/components/fields_browser/index.tsx b/x-pack/plugins/timelines/public/components/fields_browser/index.tsx index 31b8e9f62803ec..12133cbee303ea 100644 --- a/x-pack/plugins/timelines/public/components/fields_browser/index.tsx +++ b/x-pack/plugins/timelines/public/components/fields_browser/index.tsx @@ -9,9 +9,14 @@ import React from 'react'; import type { Store } from 'redux'; import { Provider } from 'react-redux'; import { I18nProvider } from '@kbn/i18n-react'; -import type { FieldBrowserProps } from '../t_grid/toolbar/fields_browser/types'; import { StatefulFieldsBrowser } from '../t_grid/toolbar/fields_browser'; -export type { FieldBrowserProps } from '../t_grid/toolbar/fields_browser/types'; +import { FieldBrowserProps } from '../../../common/types/fields_browser'; +export type { + CreateFieldComponent, + FieldBrowserOptions, + FieldBrowserProps, + GetFieldTableColumns, +} from '../../../common/types/fields_browser'; const EMPTY_BROWSER_FIELDS = {}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx index 2ae0146f80f7e5..4ba36a3ec6419b 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx @@ -47,7 +47,6 @@ import { TimelineTabs, SetEventsLoading, SetEventsDeleted, - CreateFieldComponentType, } from '../../../../common/types/timeline'; import type { TimelineItem, TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; @@ -63,10 +62,11 @@ import { import type { BrowserFields } from '../../../../common/search_strategy/index_fields'; import type { OnRowSelected, OnSelectAll } from '../types'; +import type { FieldBrowserOptions } from '../../../../common/types'; import type { Refetch } from '../../../store/t_grid/inputs'; import { getPageRowIndex } from '../../../../common/utils/pagination'; import { StatefulEventContext } from '../../../components/stateful_event_context'; -import { StatefulFieldsBrowser } from '../../../components/t_grid/toolbar/fields_browser'; +import { StatefulFieldsBrowser } from '../toolbar/fields_browser'; import { tGridActions, TGridModel, tGridSelectors, TimelineState } from '../../../store/t_grid'; import { useDeepEqualSelector } from '../../../hooks/use_selector'; import { RowAction } from './row_action'; @@ -88,10 +88,10 @@ interface OwnProps { appId?: string; browserFields: BrowserFields; bulkActions?: BulkActionsProp; - createFieldComponent?: CreateFieldComponentType; data: TimelineItem[]; defaultCellActions?: TGridCellAction[]; disabledCellActions: string[]; + fieldBrowserOptions?: FieldBrowserOptions; filters?: Filter[]; filterQuery?: string; filterStatus?: AlertStatus; @@ -149,8 +149,8 @@ const EuiDataGridContainer = styled.div<{ hideLastPage: boolean }>` const transformControlColumns = ({ columnHeaders, controlColumns, - createFieldComponent, data, + fieldBrowserOptions, isEventViewer = false, loadingEventIds, onRowSelected, @@ -171,9 +171,9 @@ const transformControlColumns = ({ }: { columnHeaders: ColumnHeaderOptions[]; controlColumns: ControlColumnProps[]; - createFieldComponent?: CreateFieldComponentType; data: TimelineItem[]; disabledCellActions: string[]; + fieldBrowserOptions?: FieldBrowserOptions; isEventViewer?: boolean; loadingEventIds: string[]; onRowSelected: OnRowSelected; @@ -209,6 +209,7 @@ const transformControlColumns = ({ )} @@ -303,10 +303,10 @@ export const BodyComponent = React.memo( bulkActions = true, clearSelected, columnHeaders, - createFieldComponent, data, defaultCellActions, disabledCellActions, + fieldBrowserOptions, filterQuery, filters, filterStatus, @@ -502,7 +502,7 @@ export const BodyComponent = React.memo( @@ -529,6 +529,7 @@ export const BodyComponent = React.memo( id, totalSelectAllAlerts, totalItems, + fieldBrowserOptions, filterStatus, filterQuery, indexNames, @@ -539,7 +540,6 @@ export const BodyComponent = React.memo( additionalControls, browserFields, columnHeaders, - createFieldComponent, ] ); @@ -629,9 +629,9 @@ export const BodyComponent = React.memo( transformControlColumns({ columnHeaders, controlColumns, - createFieldComponent, data, disabledCellActions, + fieldBrowserOptions, isEventViewer, loadingEventIds, onRowSelected, @@ -656,9 +656,9 @@ export const BodyComponent = React.memo( leadingControlColumns, trailingControlColumns, columnHeaders, - createFieldComponent, data, disabledCellActions, + fieldBrowserOptions, isEventViewer, id, loadingEventIds, diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx index b97e4047d10e7e..69c04b31fa44be 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx @@ -21,7 +21,6 @@ import type { CoreStart } from '../../../../../../../src/core/public'; import type { BrowserFields } from '../../../../common/search_strategy/index_fields'; import { BulkActionsProp, - CreateFieldComponentType, TGridCellAction, TimelineId, TimelineTabs, @@ -43,6 +42,7 @@ import { defaultHeaders } from '../body/column_headers/default_headers'; import { buildCombinedQuery, getCombinedFilterQuery, resolverIsShowing } from '../helpers'; import { tGridActions, tGridSelectors } from '../../../store/t_grid'; import { useTimelineEvents, InspectResponse, Refetch } from '../../../container'; +import { FieldBrowserOptions } from '../../fields_browser'; import { StatefulBody } from '../body'; import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexGroup, UpdatedFlexItem } from '../styles'; import { Sort } from '../body/sort'; @@ -98,7 +98,6 @@ export interface TGridIntegratedProps { browserFields: BrowserFields; bulkActions?: BulkActionsProp; columns: ColumnHeaderOptions[]; - createFieldComponent?: CreateFieldComponentType; data?: DataPublicPluginStart; dataProviders: DataProvider[]; dataViewId?: string | null; @@ -108,6 +107,7 @@ export interface TGridIntegratedProps { docValueFields: DocValueFields[]; end: string; entityType: EntityType; + fieldBrowserOptions?: FieldBrowserOptions; filters: Filter[]; filterStatus?: AlertStatus; globalFullScreen: boolean; @@ -153,12 +153,12 @@ const TGridIntegratedComponent: React.FC = ({ docValueFields, end, entityType, + fieldBrowserOptions, filters, filterStatus, globalFullScreen, graphEventId, graphOverlay = null, - createFieldComponent, hasAlertsCrud, id, indexNames, @@ -363,10 +363,10 @@ const TGridIntegratedComponent: React.FC = ({ appId={appId} browserFields={browserFields} bulkActions={bulkActions} - createFieldComponent={createFieldComponent} data={nonDeletedEvents} defaultCellActions={defaultCellActions} disabledCellActions={disabledCellActions} + fieldBrowserOptions={fieldBrowserOptions} filterQuery={filterQuery} filters={filters} filterStatus={filterStatus} diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_badges.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_badges.test.tsx new file mode 100644 index 00000000000000..e945f91c47afda --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_badges.test.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render } from '@testing-library/react'; +import React from 'react'; +import { TestProviders } from '../../../../mock'; + +import { CategoriesBadges } from './categories_badges'; + +const mockSetSelectedCategoryIds = jest.fn(); +const defaultProps = { + setSelectedCategoryIds: mockSetSelectedCategoryIds, + selectedCategoryIds: [], +}; + +describe('CategoriesBadges', () => { + beforeEach(() => { + mockSetSelectedCategoryIds.mockClear(); + }); + + it('should render empty badges', () => { + const result = render( + + + + ); + + const badges = result.getByTestId('category-badges'); + expect(badges).toBeInTheDocument(); + expect(badges.childNodes.length).toBe(0); + }); + + it('should render the selector button with selected categories', () => { + const result = render( + + + + ); + + const badges = result.getByTestId('category-badges'); + expect(badges.childNodes.length).toBe(2); + expect(result.getByTestId('category-badge-base')).toBeInTheDocument(); + expect(result.getByTestId('category-badge-event')).toBeInTheDocument(); + }); + + it('should call the set selected callback when badge unselect button clicked', () => { + const result = render( + + + + ); + + result.getByTestId('category-badge-unselect-base').click(); + expect(mockSetSelectedCategoryIds).toHaveBeenCalledWith(['event']); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_badges.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_badges.tsx new file mode 100644 index 00000000000000..14b928d18de452 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_badges.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import styled from 'styled-components'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +interface CategoriesBadgesProps { + setSelectedCategoryIds: (categoryIds: string[]) => void; + selectedCategoryIds: string[]; +} + +const CategoriesBadgesGroup = styled(EuiFlexGroup)` + margin-top: ${({ theme }) => theme.eui.euiSizeXS}; + min-height: 24px; +`; +CategoriesBadgesGroup.displayName = 'CategoriesBadgesGroup'; + +const CategoriesBadgesComponent: React.FC = ({ + setSelectedCategoryIds, + selectedCategoryIds, +}) => { + const onUnselectCategory = useCallback( + (categoryId: string) => { + setSelectedCategoryIds( + selectedCategoryIds.filter((selectedCategoryId) => selectedCategoryId !== categoryId) + ); + }, + [setSelectedCategoryIds, selectedCategoryIds] + ); + + return ( + + {selectedCategoryIds.map((categoryId) => ( + + onUnselectCategory(categoryId)} + iconOnClickAriaLabel="unselect category" + data-test-subj={`category-badge-${categoryId}`} + closeButtonProps={{ 'data-test-subj': `category-badge-unselect-${categoryId}` }} + > + {categoryId} + + + ))} + + ); +}; + +export const CategoriesBadges = React.memo(CategoriesBadgesComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.test.tsx deleted file mode 100644 index e2f1d78cf5bc2b..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.test.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mount } from 'enzyme'; -import React from 'react'; - -import { mockBrowserFields } from '../../../../mock'; - -import { CATEGORY_PANE_WIDTH } from './helpers'; -import { CategoriesPane } from './categories_pane'; -import * as i18n from './translations'; - -const timelineId = 'test'; - -describe('CategoriesPane', () => { - test('it renders the expected title', () => { - const wrapper = mount( - - ); - - expect(wrapper.find('[data-test-subj="categories-pane-title"]').first().text()).toEqual( - i18n.CATEGORIES - ); - }); - - test('it renders a "No fields match" message when filteredBrowserFields is empty', () => { - const wrapper = mount( - - ); - - expect(wrapper.find('[data-test-subj="categories-container"] tbody').first().text()).toEqual( - i18n.NO_FIELDS_MATCH - ); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.tsx deleted file mode 100644 index ffb93aee11b556..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.tsx +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiInMemoryTable, EuiTitle } from '@elastic/eui'; -import { noop } from 'lodash/fp'; -import React, { useCallback, useRef } from 'react'; -import styled from 'styled-components'; -import { - DATA_COLINDEX_ATTRIBUTE, - DATA_ROWINDEX_ATTRIBUTE, - onKeyDownFocusHandler, -} from '../../../../../common/utils/accessibility'; -import type { BrowserFields } from '../../../../../common/search_strategy'; -import { getCategoryColumns } from './category_columns'; -import { CATEGORIES_PANE_CLASS_NAME, TABLE_HEIGHT } from './helpers'; - -import * as i18n from './translations'; - -const CategoryNames = styled.div<{ height: number; width: number }>` - ${({ width }) => `width: ${width}px`}; - ${({ height }) => `height: ${height}px`}; - overflow-y: hidden; - padding: 5px; - thead { - display: none; - } -`; - -CategoryNames.displayName = 'CategoryNames'; - -const Title = styled(EuiTitle)` - padding-left: 5px; -`; - -const H3 = styled.h3` - text-align: left; -`; - -Title.displayName = 'Title'; - -interface Props { - /** - * A map of categoryId -> metadata about the fields in that category, - * filtered such that the name of every field in the category includes - * the filter input (as a substring). - */ - filteredBrowserFields: BrowserFields; - /** - * Invoked when the user clicks on the name of a category in the left-hand - * side of the field browser - */ - onCategorySelected: (categoryId: string) => void; - /** The category selected on the left-hand side of the field browser */ - selectedCategoryId: string; - timelineId: string; - /** The width of the categories pane */ - width: number; -} - -export const CategoriesPane = React.memo( - ({ filteredBrowserFields, onCategorySelected, selectedCategoryId, timelineId, width }) => { - const containerElement = useRef(null); - const onKeyDown = useCallback( - (e: React.KeyboardEvent) => { - onKeyDownFocusHandler({ - colindexAttribute: DATA_COLINDEX_ATTRIBUTE, - containerElement: containerElement?.current, - event: e, - maxAriaColindex: 1, - maxAriaRowindex: Object.keys(filteredBrowserFields).length, - onColumnFocused: noop, - rowindexAttribute: DATA_ROWINDEX_ATTRIBUTE, - }); - }, - [containerElement, filteredBrowserFields] - ); - - return ( - <> - - <H3 data-test-subj="categories-pane-title">{i18n.CATEGORIES}</H3> - - - - ({ categoryId, ariaRowindex: i + 1 }))} - message={i18n.NO_FIELDS_MATCH} - pagination={false} - sorting={false} - tableCaption={i18n.CATEGORIES} - /> - - - ); - } -); - -CategoriesPane.displayName = 'CategoriesPane'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_selector.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_selector.test.tsx new file mode 100644 index 00000000000000..eff37376a296e4 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_selector.test.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render } from '@testing-library/react'; +import React from 'react'; +import { mockBrowserFields, TestProviders } from '../../../../mock'; + +import { CategoriesSelector } from './categories_selector'; + +const mockSetSelectedCategoryIds = jest.fn(); +const defaultProps = { + filteredBrowserFields: mockBrowserFields, + setSelectedCategoryIds: mockSetSelectedCategoryIds, + selectedCategoryIds: [], +}; + +describe('CategoriesSelector', () => { + beforeEach(() => { + mockSetSelectedCategoryIds.mockClear(); + }); + + it('should render the default selector button', () => { + const categoriesCount = Object.keys(mockBrowserFields).length; + const result = render( + + + + ); + + expect(result.getByTestId('categories-filter-button')).toBeInTheDocument(); + expect(result.getByText('Categories')).toBeInTheDocument(); + expect(result.getByText(categoriesCount)).toBeInTheDocument(); + }); + + it('should render the selector button with selected categories', () => { + const result = render( + + + + ); + + expect(result.getByTestId('categories-filter-button')).toBeInTheDocument(); + expect(result.getByText('Categories')).toBeInTheDocument(); + expect(result.getByText('2')).toBeInTheDocument(); + }); + + it('should open the category selector', () => { + const result = render( + + + + ); + + result.getByTestId('categories-filter-button').click(); + + expect(result.getByTestId('categories-selector-search')).toBeInTheDocument(); + expect(result.getByTestId(`categories-selector-option-base`)).toBeInTheDocument(); + }); + + it('should open the category selector with selected categories', () => { + const result = render( + + + + ); + + result.getByTestId('categories-filter-button').click(); + + expect(result.getByTestId('categories-selector-search')).toBeInTheDocument(); + expect(result.getByTestId(`categories-selector-option-base`)).toBeInTheDocument(); + expect(result.getByTestId(`categories-selector-option-name-base`)).toHaveStyleRule( + 'font-weight', + 'bold' + ); + }); + + it('should call setSelectedCategoryIds when category selected', () => { + const result = render( + + + + ); + + result.getByTestId('categories-filter-button').click(); + result.getByTestId(`categories-selector-option-base`).click(); + expect(mockSetSelectedCategoryIds).toHaveBeenCalledWith(['base']); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_selector.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_selector.tsx new file mode 100644 index 00000000000000..6aebd32543ea3e --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_selector.tsx @@ -0,0 +1,173 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo, useState } from 'react'; +import { omit } from 'lodash'; +import { + EuiFilterButton, + EuiFilterGroup, + EuiFlexGroup, + EuiFlexItem, + EuiHighlight, + EuiPopover, + EuiSelectable, + FilterChecked, +} from '@elastic/eui'; +import { BrowserFields } from '../../../../../common'; +import * as i18n from './translations'; +import { CountBadge, getFieldCount, CategoryName, CategorySelectableContainer } from './helpers'; +import { isEscape } from '../../../../../common/utils/accessibility'; + +interface CategoriesSelectorProps { + /** + * A map of categoryId -> metadata about the fields in that category, + * filtered such that the name of every field in the category includes + * the filter input (as a substring). + */ + filteredBrowserFields: BrowserFields; + /** + * Invoked when the user clicks on the name of a category in the left-hand + * side of the field browser + */ + setSelectedCategoryIds: (categoryIds: string[]) => void; + /** The category selected on the left-hand side of the field browser */ + selectedCategoryIds: string[]; +} + +interface CategoryOption { + label: string; + count: number; + checked?: FilterChecked; +} + +const renderOption = (option: CategoryOption, searchValue: string) => { + const { label, count, checked } = option; + // Some category names have spaces, but test selectors don't like spaces, + // Tests are not able to find subjects with spaces, so we need to clean them. + const idAttr = label.replace(/\s/g, ''); + return ( + + + + {label} + + + + {count} + + + ); +}; + +const CategoriesSelectorComponent: React.FC = ({ + filteredBrowserFields, + setSelectedCategoryIds, + selectedCategoryIds, +}) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const togglePopover = useCallback(() => { + setIsPopoverOpen((open) => !open); + }, []); + const closePopover = useCallback(() => { + setIsPopoverOpen(false); + }, []); + + const totalCategories = useMemo( + () => Object.keys(filteredBrowserFields).length, + [filteredBrowserFields] + ); + + const categoryOptions: CategoryOption[] = useMemo(() => { + const unselectedCategoryIds = Object.keys( + omit(filteredBrowserFields, selectedCategoryIds) + ).sort(); + return [ + ...selectedCategoryIds.map((categoryId) => ({ + label: categoryId, + count: getFieldCount(filteredBrowserFields[categoryId]), + checked: 'on', + })), + ...unselectedCategoryIds.map((categoryId) => ({ + label: categoryId, + count: getFieldCount(filteredBrowserFields[categoryId]), + })), + ]; + }, [selectedCategoryIds, filteredBrowserFields]); + + const onCategoriesChange = useCallback( + (options: CategoryOption[]) => { + setSelectedCategoryIds( + options.filter(({ checked }) => checked === 'on').map(({ label }) => label) + ); + }, + [setSelectedCategoryIds] + ); + + const onKeyDown = useCallback((keyboardEvent: React.KeyboardEvent) => { + if (isEscape(keyboardEvent)) { + // Prevent escape to close the field browser modal after closing the category selector + keyboardEvent.stopPropagation(); + } + }, []); + + return ( + + 0} + iconType="arrowDown" + isSelected={isPopoverOpen} + numActiveFilters={selectedCategoryIds.length} + numFilters={totalCategories} + onClick={togglePopover} + > + {i18n.CATEGORIES} + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + > + + + {(list, search) => ( + <> + {search} + {list} + + )} + + + + + ); +}; + +export const CategoriesSelector = React.memo(CategoriesSelectorComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.test.tsx deleted file mode 100644 index 98f02a9484eaba..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.test.tsx +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { useMountAppended } from '../../../utils/use_mount_appended'; - -import { Category } from './category'; -import { getFieldItems } from './field_items'; -import { FIELDS_PANE_WIDTH } from './helpers'; -import { mockBrowserFields, TestProviders } from '../../../../mock'; - -import * as i18n from './translations'; - -describe('Category', () => { - const timelineId = 'test'; - const selectedCategoryId = 'client'; - const mount = useMountAppended(); - - test('it renders the category id as the value of the title', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="selected-category-title"]').first().text()).toEqual( - selectedCategoryId - ); - }); - - test('it renders the Field column header', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('.euiTableCellContent__text').at(1).text()).toEqual(i18n.FIELD); - }); - - test('it renders the Description column header', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('.euiTableCellContent__text').at(2).text()).toEqual(i18n.DESCRIPTION); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.tsx deleted file mode 100644 index 3130c46aa06843..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiInMemoryTable } from '@elastic/eui'; -import { noop } from 'lodash/fp'; -import React, { useCallback, useMemo, useRef } from 'react'; -import styled from 'styled-components'; -import { - arrayIndexToAriaIndex, - DATA_COLINDEX_ATTRIBUTE, - DATA_ROWINDEX_ATTRIBUTE, - onKeyDownFocusHandler, -} from '../../../../../common/utils/accessibility'; -import type { BrowserFields } from '../../../../../common/search_strategy'; -import type { OnUpdateColumns } from '../../../../../common/types'; - -import { CategoryTitle } from './category_title'; -import { getFieldColumns } from './field_items'; -import type { FieldItem } from './field_items'; -import { CATEGORY_TABLE_CLASS_NAME, TABLE_HEIGHT } from './helpers'; - -import * as i18n from './translations'; - -const TableContainer = styled.div<{ height: number; width: number }>` - ${({ height }) => `height: ${height}px`}; - ${({ width }) => `width: ${width}px`}; - overflow: hidden; -`; - -TableContainer.displayName = 'TableContainer'; - -/** - * This callback, invoked via `EuiInMemoryTable`'s `rowProps, assigns - * attributes to every ``. - */ -const getAriaRowindex = (fieldItem: FieldItem) => - fieldItem.ariaRowindex != null ? { 'data-rowindex': fieldItem.ariaRowindex } : {}; - -interface Props { - categoryId: string; - fieldItems: FieldItem[]; - filteredBrowserFields: BrowserFields; - onCategorySelected: (categoryId: string) => void; - onUpdateColumns: OnUpdateColumns; - timelineId: string; - width: number; -} - -export const Category = React.memo( - ({ categoryId, filteredBrowserFields, fieldItems, onUpdateColumns, timelineId, width }) => { - const containerElement = useRef(null); - const onKeyDown = useCallback( - (keyboardEvent: React.KeyboardEvent) => { - onKeyDownFocusHandler({ - colindexAttribute: DATA_COLINDEX_ATTRIBUTE, - containerElement: containerElement?.current, - event: keyboardEvent, - maxAriaColindex: 3, - maxAriaRowindex: fieldItems.length, - onColumnFocused: noop, - rowindexAttribute: DATA_ROWINDEX_ATTRIBUTE, - }); - }, - [fieldItems.length] - ); - - const fieldItemsWithRowindex = useMemo( - () => - fieldItems.map((fieldItem, i) => ({ - ...fieldItem, - ariaRowindex: arrayIndexToAriaIndex(i), - })), - [fieldItems] - ); - - const columns = useMemo(() => getFieldColumns(), []); - - return ( - <> - - - - - - - ); - } -); - -Category.displayName = 'Category'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.test.tsx deleted file mode 100644 index a94ffee597c791..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.test.tsx +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mount } from 'enzyme'; -import React from 'react'; - -import { mockBrowserFields, TestProviders } from '../../../../mock'; - -import { CATEGORY_PANE_WIDTH, getFieldCount, VIEW_ALL_BUTTON_CLASS_NAME } from './helpers'; -import { CategoriesPane } from './categories_pane'; -import { ViewAllButton } from './category_columns'; - -const timelineId = 'test'; - -describe('getCategoryColumns', () => { - Object.keys(mockBrowserFields).forEach((categoryId) => { - test(`it renders the ${categoryId} category name (from filteredBrowserFields)`, () => { - const wrapper = mount( - - ); - - const fieldCount = Object.keys(mockBrowserFields[categoryId].fields ?? {}).length; - - expect( - wrapper.find(`.field-browser-category-pane-${categoryId}-${timelineId}`).first().text() - ).toEqual(`${categoryId}${fieldCount}`); - }); - }); - - Object.keys(mockBrowserFields).forEach((categoryId) => { - test(`it renders the correct field count for the ${categoryId} category (from filteredBrowserFields)`, () => { - const wrapper = mount( - - ); - - expect( - wrapper.find(`[data-test-subj="${categoryId}-category-count"]`).first().text() - ).toEqual(`${getFieldCount(mockBrowserFields[categoryId])}`); - }); - }); - - test('it renders the selected category with bold text', () => { - const selectedCategoryId = 'auditd'; - - const wrapper = mount( - - ); - - expect( - wrapper - .find(`.field-browser-category-pane-${selectedCategoryId}-${timelineId}`) - .find('[data-test-subj="categoryName"]') - .at(1) - ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); - }); - - test('it does NOT render an un-selected category with bold text', () => { - const selectedCategoryId = 'auditd'; - const notTheSelectedCategoryId = 'base'; - - const wrapper = mount( - - ); - - expect( - wrapper - .find(`.field-browser-category-pane-${notTheSelectedCategoryId}-${timelineId}`) - .find('[data-test-subj="categoryName"]') - .at(1) - ).toHaveStyleRule('font-weight', 'normal', { modifier: '.euiText' }); - }); - - test('it invokes onCategorySelected when a user clicks a category', () => { - const selectedCategoryId = 'auditd'; - const notTheSelectedCategoryId = 'base'; - - const onCategorySelected = jest.fn(); - - const wrapper = mount( - - ); - - wrapper - .find(`.field-browser-category-pane-${notTheSelectedCategoryId}-${timelineId}`) - .first() - .simulate('click'); - - expect(onCategorySelected).toHaveBeenCalledWith(notTheSelectedCategoryId); - }); -}); - -describe('ViewAllButton', () => { - it(`should update fields with the timestamp and category fields`, () => { - const onUpdateColumns = jest.fn(); - - const wrapper = mount( - - - - ); - - wrapper.find(`.${VIEW_ALL_BUTTON_CLASS_NAME}`).first().simulate('click'); - - expect(onUpdateColumns).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ id: '@timestamp' }), - expect.objectContaining({ id: 'agent.ephemeral_id' }), - expect.objectContaining({ id: 'agent.hostname' }), - expect.objectContaining({ id: 'agent.id' }), - expect.objectContaining({ id: 'agent.name' }), - ]) - ); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.tsx deleted file mode 100644 index 0fdf71ff5ffe1a..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.tsx +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiLink, - EuiText, - EuiToolTip, -} from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; -import styled from 'styled-components'; - -import { useDeepEqualSelector } from '../../../../hooks/use_selector'; -import { - LoadingSpinner, - getCategoryPaneCategoryClassName, - getFieldCount, - VIEW_ALL_BUTTON_CLASS_NAME, - CountBadge, -} from './helpers'; -import * as i18n from './translations'; -import { tGridSelectors } from '../../../../store/t_grid'; -import { getColumnsWithTimestamp } from '../../../utils/helpers'; -import type { BrowserFields } from '../../../../../common/search_strategy'; -import type { OnUpdateColumns } from '../../../../../common/types'; - -const CategoryName = styled.span<{ bold: boolean }>` - .euiText { - font-weight: ${({ bold }) => (bold ? 'bold' : 'normal')}; - } -`; - -CategoryName.displayName = 'CategoryName'; - -const LinkContainer = styled.div` - width: 100%; - .euiLink { - width: 100%; - } -`; - -LinkContainer.displayName = 'LinkContainer'; - -const ViewAll = styled(EuiButtonIcon)` - margin-left: 2px; -`; - -ViewAll.displayName = 'ViewAll'; - -export interface CategoryItem { - categoryId: string; -} - -interface ViewAllButtonProps { - categoryId: string; - browserFields: BrowserFields; - onUpdateColumns: OnUpdateColumns; - timelineId: string; -} - -export const ViewAllButton = React.memo( - ({ categoryId, browserFields, onUpdateColumns, timelineId }) => { - const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []); - const { isLoading } = useDeepEqualSelector((state) => - getManageTimeline(state, timelineId ?? '') - ); - - const handleClick = useCallback(() => { - onUpdateColumns( - getColumnsWithTimestamp({ - browserFields, - category: categoryId, - }) - ); - }, [browserFields, categoryId, onUpdateColumns]); - - return ( - - {!isLoading ? ( - - ) : ( - - )} - - ); - } -); - -ViewAllButton.displayName = 'ViewAllButton'; - -/** - * Returns the column definition for the (single) column that displays all the - * category names in the field browser */ -export const getCategoryColumns = ({ - filteredBrowserFields, - onCategorySelected, - selectedCategoryId, - timelineId, -}: { - filteredBrowserFields: BrowserFields; - onCategorySelected: (categoryId: string) => void; - selectedCategoryId: string; - timelineId: string; -}) => [ - { - field: 'categoryId', - name: '', - sortable: true, - truncateText: false, - render: ( - categoryId: string, - { ariaRowindex }: { categoryId: string; ariaRowindex: number } - ) => ( - - onCategorySelected(categoryId)} - > - - - - {categoryId} - - - - - - {getFieldCount(filteredBrowserFields[categoryId])} - - - - - - ), - }, -]; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.test.tsx deleted file mode 100644 index 746668491abb84..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.test.tsx +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mount } from 'enzyme'; -import React from 'react'; - -import { mockBrowserFields, TestProviders } from '../../../../mock'; - -import { CategoryTitle } from './category_title'; -import { getFieldCount } from './helpers'; - -describe('CategoryTitle', () => { - const timelineId = 'test'; - - test('it renders the category id as the value of the title', () => { - const categoryId = 'client'; - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="selected-category-title"]').first().text()).toEqual( - categoryId - ); - }); - - test('when `categoryId` specifies a valid category in `filteredBrowserFields`, a count of the field is displayed in the badge', () => { - const validCategoryId = 'client'; - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="selected-category-count-badge"]`).first().text()).toEqual( - `${getFieldCount(mockBrowserFields[validCategoryId])}` - ); - }); - - test('when `categoryId` specifies an INVALID category in `filteredBrowserFields`, a count of zero is displayed in the badge', () => { - const invalidCategoryId = 'this.is.not.happening'; - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="selected-category-count-badge"]`).first().text()).toEqual( - '0' - ); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.tsx deleted file mode 100644 index 0858f30a352463..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFlexGroup, EuiFlexItem, EuiScreenReaderOnly, EuiTitle } from '@elastic/eui'; -import React from 'react'; - -import { CountBadge, getFieldBrowserCategoryTitleClassName, getFieldCount } from './helpers'; -import type { BrowserFields } from '../../../../../common/search_strategy'; -import type { OnUpdateColumns } from '../../../../../common/types'; - -import { ViewAllButton } from './category_columns'; -import * as i18n from './translations'; - -interface Props { - /** The title of the category */ - categoryId: string; - /** - * A map of categoryId -> metadata about the fields in that category, - * filtered such that the name of every field in the category includes - * the filter input (as a substring). - */ - filteredBrowserFields: BrowserFields; - onUpdateColumns: OnUpdateColumns; - - /** The timeline associated with this field browser */ - timelineId: string; -} - -export const CategoryTitle = React.memo( - ({ filteredBrowserFields, categoryId, onUpdateColumns, timelineId }) => ( - - - -

{i18n.CATEGORY}

-
- -

{categoryId}

-
-
- - - - {getFieldCount(filteredBrowserFields[categoryId])} - - - - - - -
- ) -); - -CategoryTitle.displayName = 'CategoryTitle'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx index d435d7a280840b..ed665155ddcf52 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx @@ -34,18 +34,20 @@ const testProps = { searchInput: '', appliedFilterInput: '', isSearching: false, - onCategorySelected: jest.fn(), + setSelectedCategoryIds: jest.fn(), onHide, onSearchInputChange: jest.fn(), restoreFocusTo: React.createRef(), - selectedCategoryId: '', + selectedCategoryIds: [], timelineId, }; const { storage } = createSecuritySolutionStorageMock(); + describe('FieldsBrowser', () => { beforeEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); + test('it renders the Close button', () => { const wrapper = mount( @@ -80,20 +82,7 @@ describe('FieldsBrowser', () => { test('it invokes updateColumns action when the user clicks the Reset Fields button', () => { const wrapper = mount( - ()} - selectedCategoryId={''} - timelineId={timelineId} - /> + ); @@ -129,24 +118,24 @@ describe('FieldsBrowser', () => { expect(wrapper.find('[data-test-subj="field-search"]').exists()).toBe(true); }); - test('it renders the categories pane', () => { + test('it renders the categories selector', () => { const wrapper = mount( ); - expect(wrapper.find('[data-test-subj="left-categories-pane"]').exists()).toBe(true); + expect(wrapper.find('[data-test-subj="categories-selector"]').exists()).toBe(true); }); - test('it renders the fields pane', () => { + test('it renders the fields table', () => { const wrapper = mount( ); - expect(wrapper.find('[data-test-subj="fields-pane"]').exists()).toBe(true); + expect(wrapper.find('[data-test-subj="field-table"]').exists()).toBe(true); }); test('focuses the search input when the component mounts', () => { @@ -183,19 +172,24 @@ describe('FieldsBrowser', () => { expect(onSearchInputChange).toBeCalledWith(inputText); }); - test('does not render the CreateField button when createFieldComponent is provided without a dataViewId', () => { + test('does not render the CreateFieldButton when it is provided but does not have a dataViewId', () => { const MyTestComponent = () =>
{'test'}
; const wrapper = mount( - + ); expect(wrapper.find(MyTestComponent).exists()).toBeFalsy(); }); - test('it renders the CreateField button when createFieldComponent is provided with a dataViewId', () => { + test('it renders the CreateFieldButton when it is provided and have a dataViewId', () => { const state: State = { ...mockGlobalState, timelineById: { @@ -212,7 +206,12 @@ describe('FieldsBrowser', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx index e55f54e946ad13..5a01c820aa9619 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx @@ -17,51 +17,27 @@ import { EuiButtonEmpty, EuiSpacer, } from '@elastic/eui'; -import React, { useEffect, useCallback, useRef, useMemo } from 'react'; -import styled from 'styled-components'; +import React, { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import type { BrowserFields } from '../../../../../common/search_strategy'; -import type { ColumnHeaderOptions, CreateFieldComponentType } from '../../../../../common/types'; -import { - isEscape, - isTab, - stopPropagationAndPreventDefault, -} from '../../../../../common/utils/accessibility'; -import { CategoriesPane } from './categories_pane'; -import { FieldsPane } from './fields_pane'; +import type { FieldBrowserProps, ColumnHeaderOptions } from '../../../../../common/types'; import { Search } from './search'; -import { - CATEGORY_PANE_WIDTH, - CLOSE_BUTTON_CLASS_NAME, - FIELDS_PANE_WIDTH, - FIELD_BROWSER_WIDTH, - focusSearchInput, - onFieldsBrowserTabPressed, - PANES_FLEX_GROUP_WIDTH, - RESET_FIELDS_CLASS_NAME, - scrollCategoriesPane, -} from './helpers'; -import type { FieldBrowserProps } from './types'; +import { CLOSE_BUTTON_CLASS_NAME, FIELD_BROWSER_WIDTH, RESET_FIELDS_CLASS_NAME } from './helpers'; import { tGridActions, tGridSelectors } from '../../../../store/t_grid'; import * as i18n from './translations'; import { useDeepEqualSelector } from '../../../../hooks/use_selector'; +import { CategoriesSelector } from './categories_selector'; +import { FieldTable } from './field_table'; +import { CategoriesBadges } from './categories_badges'; -const PanesFlexGroup = styled(EuiFlexGroup)` - width: ${PANES_FLEX_GROUP_WIDTH}px; -`; -PanesFlexGroup.displayName = 'PanesFlexGroup'; - -type Props = Pick & { +type Props = Pick & { /** * The current timeline column headers */ columnHeaders: ColumnHeaderOptions[]; - - createFieldComponent?: CreateFieldComponentType; - /** * A map of categoryId -> metadata about the fields in that category, * filtered such that the name of every field in the category includes @@ -80,12 +56,12 @@ type Props = Pick & /** * The category selected on the left-hand side of the field browser */ - selectedCategoryId: string; + selectedCategoryIds: string[]; /** * Invoked when the user clicks on the name of a category in the left-hand * side of the field browser */ - onCategorySelected: (categoryId: string) => void; + setSelectedCategoryIds: (categoryIds: string[]) => void; /** * Hides the field browser when invoked */ @@ -110,23 +86,23 @@ type Props = Pick & const FieldsBrowserComponent: React.FC = ({ columnHeaders, filteredBrowserFields, - createFieldComponent: CreateField, isSearching, - onCategorySelected, + setSelectedCategoryIds, onSearchInputChange, onHide, + options, restoreFocusTo, searchInput, appliedFilterInput, - selectedCategoryId, + selectedCategoryIds, timelineId, width = FIELD_BROWSER_WIDTH, }) => { const dispatch = useDispatch(); - const containerElement = useRef(null); const onUpdateColumns = useCallback( - (columns) => dispatch(tGridActions.updateColumns({ id: timelineId, columns })), + (columns: ColumnHeaderOptions[]) => + dispatch(tGridActions.updateColumns({ id: timelineId, columns })), [dispatch, timelineId] ); @@ -156,45 +132,14 @@ const FieldsBrowserComponent: React.FC = ({ [onSearchInputChange] ); - const scrollViewsAndFocusInput = useCallback(() => { - scrollCategoriesPane({ - containerElement: containerElement.current, - selectedCategoryId, - timelineId, - }); - - // always re-focus the input to enable additional filtering - focusSearchInput({ - containerElement: containerElement.current, - timelineId, - }); - }, [selectedCategoryId, timelineId]); - - useEffect(() => { - scrollViewsAndFocusInput(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedCategoryId, timelineId]); - - const onKeyDown = useCallback( - (keyboardEvent: React.KeyboardEvent) => { - if (isEscape(keyboardEvent)) { - stopPropagationAndPreventDefault(keyboardEvent); - closeAndRestoreFocus(); - } else if (isTab(keyboardEvent)) { - onFieldsBrowserTabPressed({ - containerElement: containerElement.current, - keyboardEvent, - selectedCategoryId, - timelineId, - }); - } - }, - [closeAndRestoreFocus, containerElement, selectedCategoryId, timelineId] - ); + const [CreateFieldButton, getFieldTableColumns] = [ + options?.createFieldButton, + options?.getFieldTableColumns, + ]; return ( -
+

{i18n.FIELDS_BROWSER}

@@ -202,11 +147,10 @@ const FieldsBrowserComponent: React.FC = ({
- + = ({ /> - {CreateField && dataViewId != null && dataViewId.length > 0 && ( - + + + + {CreateFieldButton && dataViewId != null && dataViewId.length > 0 && ( + )} + + - - - - - - - - + diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.test.tsx index a4c830c3d8808a..45b122354528b4 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.test.tsx @@ -5,21 +5,17 @@ * 2.0. */ -import { omit } from 'lodash/fp'; import React from 'react'; -import { waitFor } from '@testing-library/react'; -import { mockBrowserFields, TestProviders } from '../../../../mock'; +import { omit } from 'lodash/fp'; +import { render } from '@testing-library/react'; +import { EuiInMemoryTable } from '@elastic/eui'; +import { mockBrowserFields } from '../../../../mock'; import { defaultColumnHeaderType } from '../../body/column_headers/default_headers'; import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../../body/constants'; -import { Category } from './category'; import { getFieldColumns, getFieldItems } from './field_items'; -import { FIELDS_PANE_WIDTH } from './helpers'; -import { useMountAppended } from '../../../utils/use_mount_appended'; import { ColumnHeaderOptions } from '../../../../../common/types'; -const selectedCategoryId = 'base'; -const selectedCategoryFields = mockBrowserFields[selectedCategoryId].fields; const timestampFieldId = '@timestamp'; const columnHeaders: ColumnHeaderOptions[] = [ { @@ -28,7 +24,7 @@ const columnHeaders: ColumnHeaderOptions[] = [ description: 'Date/time when the event originated.\nFor log events this is the date/time when the event was generated, and not when it was read.\nRequired field for all events.', example: '2016-05-23T08:05:34.853Z', - id: '@timestamp', + id: timestampFieldId, type: 'date', aggregatable: true, initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH, @@ -36,295 +32,199 @@ const columnHeaders: ColumnHeaderOptions[] = [ ]; describe('field_items', () => { - const timelineId = 'test'; - const mount = useMountAppended(); - describe('getFieldItems', () => { - Object.keys(selectedCategoryFields!).forEach((fieldId) => { - test(`it renders the name of the ${fieldId} field`, () => { - const wrapper = mount( - - - - ); + const timestampField = mockBrowserFields.base.fields![timestampFieldId]; - expect(wrapper.find(`[data-test-subj="field-name-${fieldId}"]`).first().text()).toEqual( - fieldId - ); + it('should return browser field item format', () => { + const fieldItems = getFieldItems({ + selectedCategoryIds: ['base'], + browserFields: { base: { fields: { [timestampFieldId]: timestampField } } }, + columnHeaders: [], }); - }); - Object.keys(selectedCategoryFields!).forEach((fieldId) => { - test(`it renders a checkbox for the ${fieldId} field`, () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="field-${fieldId}-checkbox"]`).first().exists()).toBe( - true - ); + expect(fieldItems[0]).toEqual({ + name: timestampFieldId, + description: timestampField.description, + category: 'base', + selected: false, + type: timestampField.type, + example: timestampField.example, + isRuntime: false, }); }); - test('it renders a checkbox in the checked state when the field is selected to be displayed as a column in the timeline', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`[data-test-subj="field-${timestampFieldId}-checkbox"]`).first().props() - .checked - ).toBe(true); - }); - - test('it does NOT render a checkbox in the checked state when the field is NOT selected to be displayed as a column in the timeline', () => { - const wrapper = mount( - - header.id !== timestampFieldId), - highlight: '', - timelineId, - toggleColumn: jest.fn(), - })} - width={FIELDS_PANE_WIDTH} - onCategorySelected={jest.fn()} - onUpdateColumns={jest.fn()} - timelineId={timelineId} - /> - - ); - - expect( - wrapper.find(`[data-test-subj="field-${timestampFieldId}-checkbox"]`).first().props() - .checked - ).toBe(false); - }); - - test('it invokes `toggleColumn` when the user interacts with the checkbox', () => { - const toggleColumn = jest.fn(); - - const wrapper = mount( - - - - ); - - wrapper - .find('input[type="checkbox"]') - .first() - .simulate('change', { - target: { checked: true }, - }); - wrapper.update(); + it('should return selected item', () => { + const fieldItems = getFieldItems({ + selectedCategoryIds: ['base'], + browserFields: { base: { fields: { [timestampFieldId]: timestampField } } }, + columnHeaders, + }); - expect(toggleColumn).toBeCalledWith({ - columnHeaderType: 'not-filtered', - id: '@timestamp', - initialWidth: 180, + expect(fieldItems[0]).toMatchObject({ + selected: true, }); }); - test('it returns the expected signal column settings', async () => { - const mockSelectedCategoryId = 'signal'; - const mockBrowserFieldsWithSignal = { - ...mockBrowserFields, - signal: { - fields: { - 'signal.rule.name': { - aggregatable: true, - category: 'signal', - description: 'rule name', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'signal.rule.name', - searchable: true, - type: 'string', + it('should return isRuntime field', () => { + const fieldItems = getFieldItems({ + selectedCategoryIds: ['base'], + browserFields: { + base: { + fields: { + [timestampFieldId]: { + ...timestampField, + runtimeField: { type: 'keyword', script: { source: 'scripts are fun' } }, + }, }, }, }, - }; - const toggleColumn = jest.fn(); - const wrapper = mount( - - - - ); - wrapper - .find(`[data-test-subj="field-signal.rule.name-checkbox"]`) - .last() - .simulate('change', { - target: { checked: true }, - }); + columnHeaders, + }); - await waitFor(() => { - expect(toggleColumn).toBeCalledWith({ - columnHeaderType: 'not-filtered', - id: 'signal.rule.name', - initialWidth: 180, - }); + expect(fieldItems[0]).toMatchObject({ + isRuntime: true, }); }); - test('it renders the expected icon for a field', () => { - const wrapper = mount( - - - + it('should return all field items of all categories if no category selected', () => { + const fieldCount = Object.values(mockBrowserFields).reduce( + (total, { fields }) => total + Object.keys(fields ?? {}).length, + 0 ); - expect( - wrapper.find(`[data-test-subj="field-${timestampFieldId}-icon"]`).first().props().type - ).toEqual('clock'); + const fieldItems = getFieldItems({ + selectedCategoryIds: [], + browserFields: mockBrowserFields, + columnHeaders: [], + }); + + expect(fieldItems.length).toBe(fieldCount); }); - test('it renders the expected field description', () => { - const wrapper = mount( - - - + it('should return filtered field items of selected categories', () => { + const selectedCategoryIds = ['base', 'event']; + const fieldCount = selectedCategoryIds.reduce( + (total, selectedCategoryId) => + total + Object.keys(mockBrowserFields[selectedCategoryId].fields ?? {}).length, + 0 ); - expect( - wrapper.find(`[data-test-subj="field-${timestampFieldId}-description"]`).first().text() - ).toEqual( - 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events. Example: 2016-05-23T08:05:34.853Z' - ); + const fieldItems = getFieldItems({ + selectedCategoryIds, + browserFields: mockBrowserFields, + columnHeaders: [], + }); + + expect(fieldItems.length).toBe(fieldCount); }); }); describe('getFieldColumns', () => { - test('it returns the expected column definitions', () => { - expect(getFieldColumns().map((column) => omit('render', column))).toEqual([ + const onToggleColumn = jest.fn(); + + beforeEach(() => { + onToggleColumn.mockClear(); + }); + + it('should return default field columns', () => { + expect(getFieldColumns({ onToggleColumn }).map((column) => omit('render', column))).toEqual([ { - field: 'checkbox', + field: 'selected', name: '', sortable: false, width: '25px', }, - { field: 'field', name: 'Field', sortable: false, width: '225px' }, + { + field: 'name', + name: 'Name', + sortable: true, + width: '225px', + }, { field: 'description', name: 'Description', + sortable: true, + width: '400px', + }, + { + field: 'category', + name: 'Category', + sortable: true, + width: '100px', + }, + ]); + }); + + it('should return custom field columns', () => { + const customColumns = [ + { + field: 'name', + name: 'customColumn1', sortable: false, - truncateText: true, + width: '225px', + }, + { + field: 'description', + name: 'customColumn2', + sortable: true, width: '400px', }, + ]; + + expect( + getFieldColumns({ + onToggleColumn, + getFieldTableColumns: () => customColumns, + }).map((column) => omit('render', column)) + ).toEqual([ + { + field: 'selected', + name: '', + sortable: false, + width: '25px', + }, + ...customColumns, ]); }); + + it('should render default columns', () => { + const timestampField = mockBrowserFields.base.fields![timestampFieldId]; + const fieldItems = getFieldItems({ + selectedCategoryIds: ['base'], + browserFields: { base: { fields: { [timestampFieldId]: timestampField } } }, + columnHeaders: [], + }); + + const columns = getFieldColumns({ onToggleColumn }); + const { getByTestId, getAllByText } = render( + + ); + + expect(getAllByText('Name').at(0)).toBeInTheDocument(); + expect(getAllByText('Description').at(0)).toBeInTheDocument(); + expect(getAllByText('Category').at(0)).toBeInTheDocument(); + + expect(getByTestId(`field-${timestampFieldId}-checkbox`)).toBeInTheDocument(); + expect(getByTestId(`field-${timestampFieldId}-name`)).toBeInTheDocument(); + expect(getByTestId(`field-${timestampFieldId}-description`)).toBeInTheDocument(); + expect(getByTestId(`field-${timestampFieldId}-category`)).toBeInTheDocument(); + }); + + it('should call call toggle callback on checkbox click', () => { + const timestampField = mockBrowserFields.base.fields![timestampFieldId]; + const fieldItems = getFieldItems({ + selectedCategoryIds: ['base'], + browserFields: { base: { fields: { [timestampFieldId]: timestampField } } }, + columnHeaders: [], + }); + + const columns = getFieldColumns({ onToggleColumn }); + const { getByTestId } = render( + + ); + + getByTestId(`field-${timestampFieldId}-checkbox`).click(); + expect(onToggleColumn).toHaveBeenCalledWith(timestampFieldId); + }); }); }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.tsx index a979e209bf64aa..1e066eb2174a53 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.tsx @@ -13,14 +13,22 @@ import { EuiFlexGroup, EuiFlexItem, EuiScreenReaderOnly, + EuiBadge, + EuiBasicTableColumn, + EuiTableActionsColumnType, } from '@elastic/eui'; import { uniqBy } from 'lodash/fp'; import styled from 'styled-components'; import { getEmptyValue } from '../../../empty_value'; import { getExampleText, getIconFromType } from '../../../utils/helpers'; -import type { BrowserField } from '../../../../../common/search_strategy'; -import type { ColumnHeaderOptions } from '../../../../../common/types'; +import type { BrowserFields } from '../../../../../common/search_strategy'; +import type { + ColumnHeaderOptions, + BrowserFieldItem, + FieldTableColumns, + GetFieldTableColumns, +} from '../../../../../common/types'; import { defaultColumnHeaderType } from '../../body/column_headers/default_headers'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../../body/constants'; import { TruncatableText } from '../../../truncatable_text'; @@ -33,125 +41,155 @@ const TypeIcon = styled(EuiIcon)` position: relative; top: -1px; `; - TypeIcon.displayName = 'TypeIcon'; export const Description = styled.span` user-select: text; width: 400px; `; - Description.displayName = 'Description'; /** - * An item rendered in the table - */ -export interface FieldItem { - ariaRowindex?: number; - checkbox: React.ReactNode; - description: React.ReactNode; - field: React.ReactNode; - fieldId: string; -} - -/** - * Returns the fields items, values, and descriptions shown when a user expands an event + * Returns the field items of all categories selected */ export const getFieldItems = ({ - category, + browserFields, + selectedCategoryIds, columnHeaders, - highlight = '', - timelineId, - toggleColumn, }: { - category: Partial; + browserFields: BrowserFields; + selectedCategoryIds: string[]; columnHeaders: ColumnHeaderOptions[]; - highlight?: string; - timelineId: string; - toggleColumn: (column: ColumnHeaderOptions) => void; -}): FieldItem[] => - uniqBy('name', [ - ...Object.values(category != null && category.fields != null ? category.fields : {}), - ]).map((field) => ({ - checkbox: ( - - c.id === field.name) !== -1} - data-test-subj={`field-${field.name}-checkbox`} - data-colindex={1} - id={field.name ?? ''} - onChange={() => - toggleColumn({ - columnHeaderType: defaultColumnHeaderType, - id: field.name ?? '', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - ...getAlertColumnHeader(timelineId, field.name ?? ''), - }) - } - /> - - ), - field: ( - - - - - - +}): BrowserFieldItem[] => { + const categoryIds = + selectedCategoryIds.length > 0 ? selectedCategoryIds : Object.keys(browserFields); + const selectedFieldIds = new Set(columnHeaders.map(({ id }) => id)); - - - - + return uniqBy( + 'name', + categoryIds.reduce((fieldItems, categoryId) => { + const categoryBrowserFields = Object.values(browserFields[categoryId]?.fields ?? {}); + if (categoryBrowserFields.length > 0) { + fieldItems.push( + ...categoryBrowserFields.map(({ name = '', ...field }) => ({ + name, + type: field.type, + description: field.description ?? '', + example: field.example?.toString(), + category: categoryId, + selected: selectedFieldIds.has(name), + isRuntime: !!field.runtimeField, + })) + ); + } + return fieldItems; + }, []) + ); +}; + +/** + * Returns the column header for a field + */ +export const getColumnHeader = (timelineId: string, fieldName: string): ColumnHeaderOptions => ({ + columnHeaderType: defaultColumnHeaderType, + id: fieldName, + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + ...getAlertColumnHeader(timelineId, fieldName), +}); + +const getDefaultFieldTableColumns = (highlight: string): FieldTableColumns => [ + { + field: 'name', + name: i18n.NAME, + render: (name: string, { type }) => { + return ( + + + + + + + + + + + + ); + }, + sortable: true, + width: '225px', + }, + { + field: 'description', + name: i18n.DESCRIPTION, + render: (description: string, { name, example }) => ( + + <> + +

{i18n.DESCRIPTION_FOR_FIELD(name)}

+
+ + + {`${description ?? getEmptyValue()} ${getExampleText(example)}`} + + + +
), - description: ( -
- - <> - -

{i18n.DESCRIPTION_FOR_FIELD(field.name ?? '')}

-
- - - {`${field.description ?? getEmptyValue()} ${getExampleText(field.example)}`} - - - -
-
+ sortable: true, + width: '400px', + }, + { + field: 'category', + name: i18n.CATEGORY, + render: (category: string, { name }) => ( + {category} ), - fieldId: field.name ?? '', - })); + sortable: true, + width: '100px', + }, +]; /** * Returns a table column template provided to the `EuiInMemoryTable`'s * `columns` prop */ -export const getFieldColumns = () => [ +export const getFieldColumns = ({ + onToggleColumn, + highlight = '', + getFieldTableColumns, +}: { + onToggleColumn: (id: string) => void; + highlight?: string; + getFieldTableColumns?: GetFieldTableColumns; +}): FieldTableColumns => [ { - field: 'checkbox', + field: 'selected', name: '', - render: (checkbox: React.ReactNode, _: FieldItem) => checkbox, + render: (selected: boolean, { name }) => ( + + onToggleColumn(name)} + /> + + ), sortable: false, width: '25px', }, - { - field: 'field', - name: i18n.FIELD, - render: (field: React.ReactNode, _: FieldItem) => field, - sortable: false, - width: '225px', - }, - { - field: 'description', - name: i18n.DESCRIPTION, - render: (description: React.ReactNode, _: FieldItem) => description, - sortable: false, - truncateText: true, - width: '400px', - }, + ...(getFieldTableColumns + ? getFieldTableColumns(highlight) + : getDefaultFieldTableColumns(highlight)), ]; + +/** Returns whether the table column has actions attached to it */ +export const isActionsColumn = (column: EuiBasicTableColumn): boolean => { + return !!(column as EuiTableActionsColumnType).actions?.length; +}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.test.tsx index 05f093eaf1805d..6bda5873edc257 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.test.tsx @@ -43,7 +43,7 @@ describe('FieldName', () => { ); expect( - wrapper.find(`[data-test-subj="field-name-${timestampFieldId}"]`).first().text() + wrapper.find(`[data-test-subj="field-${timestampFieldId}-name"]`).first().text() ).toEqual(timestampFieldId); }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.tsx index 5781211058d3c3..0ef0ce64c637b8 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.tsx @@ -15,7 +15,7 @@ export const FieldName = React.memo<{ }>(({ fieldId, highlight = '' }) => { return ( - + {fieldId} diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.test.tsx new file mode 100644 index 00000000000000..14f2151d240747 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.test.tsx @@ -0,0 +1,225 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { mockBrowserFields, TestProviders } from '../../../../mock'; +import { tGridActions } from '../../../../store/t_grid'; +import { defaultColumnHeaderType } from '../../body/column_headers/default_headers'; +import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../../body/constants'; + +import { ColumnHeaderOptions } from '../../../../../common'; +import { FieldTable } from './field_table'; + +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + +const timestampFieldId = '@timestamp'; + +const columnHeaders: ColumnHeaderOptions[] = [ + { + category: 'base', + columnHeaderType: defaultColumnHeaderType, + description: + 'Date/time when the event originated.\nFor log events this is the date/time when the event was generated, and not when it was read.\nRequired field for all events.', + example: '2016-05-23T08:05:34.853Z', + id: timestampFieldId, + type: 'date', + aggregatable: true, + initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH, + }, +]; + +describe('FieldTable', () => { + const timelineId = 'test'; + const timestampField = mockBrowserFields.base.fields![timestampFieldId]; + const defaultPageSize = 10; + const totalFields = Object.values(mockBrowserFields).reduce( + (total, { fields }) => total + Object.keys(fields ?? {}).length, + 0 + ); + + beforeEach(() => { + mockDispatch.mockClear(); + }); + + it('should render empty field table', () => { + const result = render( + + + + ); + + expect(result.getByText('No items found')).toBeInTheDocument(); + expect(result.getByTestId('fields-count').textContent).toContain('0'); + }); + + it('should render field table with fields of all categories', () => { + const result = render( + + + + ); + + expect(result.container.getElementsByClassName('euiTableRow').length).toBe(defaultPageSize); + expect(result.getByTestId('fields-count').textContent).toContain(totalFields); + }); + + it('should render field table with fields of categories selected', () => { + const selectedCategoryIds = ['client', 'event']; + + const fieldCount = selectedCategoryIds.reduce( + (total, selectedCategoryId) => + total + Object.keys(mockBrowserFields[selectedCategoryId].fields ?? {}).length, + 0 + ); + + const result = render( + + + + ); + + expect(result.container.getElementsByClassName('euiTableRow').length).toBe(fieldCount); + expect(result.getByTestId('fields-count').textContent).toContain(fieldCount); + }); + + it('should render field table with custom columns', () => { + const fieldTableColumns = [ + { + field: 'name', + name: 'Custom column', + render: () =>
, + }, + ]; + + const result = render( + + fieldTableColumns} + selectedCategoryIds={[]} + columnHeaders={[]} + filteredBrowserFields={mockBrowserFields} + searchInput="" + timelineId={timelineId} + /> + + ); + + expect(result.getByTestId('fields-count').textContent).toContain(totalFields); + expect(result.getAllByText('Custom column').length).toBeGreaterThan(0); + expect(result.getAllByTestId('customColumn').length).toEqual(defaultPageSize); + }); + + it('should render field table with unchecked field', () => { + const result = render( + + + + ); + + const checkbox = result.getByTestId(`field-${timestampFieldId}-checkbox`); + expect(checkbox).not.toHaveAttribute('checked'); + }); + + it('should render field table with checked field', () => { + const result = render( + + + + ); + + const checkbox = result.getByTestId(`field-${timestampFieldId}-checkbox`); + expect(checkbox).toHaveAttribute('checked'); + }); + + it('should dispatch remove column action on field unchecked', () => { + const result = render( + + + + ); + + result.getByTestId(`field-${timestampFieldId}-checkbox`).click(); + + expect(mockDispatch).toHaveBeenCalledTimes(1); + expect(mockDispatch).toHaveBeenCalledWith( + tGridActions.removeColumn({ id: timelineId, columnId: timestampFieldId }) + ); + }); + + it('should dispatch upsert column action on field checked', () => { + const result = render( + + + + ); + + result.getByTestId(`field-${timestampFieldId}-checkbox`).click(); + + expect(mockDispatch).toHaveBeenCalledTimes(1); + expect(mockDispatch).toHaveBeenCalledWith( + tGridActions.upsertColumn({ + id: timelineId, + column: { + columnHeaderType: defaultColumnHeaderType, + id: timestampFieldId, + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + index: 1, + }) + ); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.tsx new file mode 100644 index 00000000000000..332422ed664f6c --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.tsx @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo } from 'react'; +import styled from 'styled-components'; +import { EuiInMemoryTable, EuiText } from '@elastic/eui'; +import { useDispatch } from 'react-redux'; +import { BrowserFields, ColumnHeaderOptions } from '../../../../../common'; +import * as i18n from './translations'; +import { getColumnHeader, getFieldColumns, getFieldItems, isActionsColumn } from './field_items'; +import { CATEGORY_TABLE_CLASS_NAME, TABLE_HEIGHT } from './helpers'; +import { tGridActions } from '../../../../store/t_grid'; +import type { GetFieldTableColumns } from '../../../../../common/types/fields_browser'; + +interface FieldTableProps { + timelineId: string; + columnHeaders: ColumnHeaderOptions[]; + /** + * A map of categoryId -> metadata about the fields in that category, + * filtered such that the name of every field in the category includes + * the filter input (as a substring). + */ + filteredBrowserFields: BrowserFields; + /** + * Optional function to customize field table columns + */ + getFieldTableColumns?: GetFieldTableColumns; + /** + * The category selected on the left-hand side of the field browser + */ + selectedCategoryIds: string[]; + /** The text displayed in the search input */ + /** Invoked when a user chooses to view a new set of columns in the timeline */ + searchInput: string; +} + +const TableContainer = styled.div<{ height: number }>` + margin-top: ${({ theme }) => theme.eui.euiSizeXS}; + border-top: ${({ theme }) => theme.eui.euiBorderThin}; + ${({ height }) => `height: ${height}px`}; + overflow: hidden; +`; +TableContainer.displayName = 'TableContainer'; + +const Count = styled.span` + font-weight: bold; +`; +Count.displayName = 'Count'; + +const FieldTableComponent: React.FC = ({ + columnHeaders, + filteredBrowserFields, + getFieldTableColumns, + searchInput, + selectedCategoryIds, + timelineId, +}) => { + const dispatch = useDispatch(); + + const fieldItems = useMemo( + () => + getFieldItems({ + browserFields: filteredBrowserFields, + selectedCategoryIds, + columnHeaders, + }), + [columnHeaders, filteredBrowserFields, selectedCategoryIds] + ); + + const onToggleColumn = useCallback( + (fieldId: string) => { + if (columnHeaders.some(({ id }) => id === fieldId)) { + dispatch( + tGridActions.removeColumn({ + columnId: fieldId, + id: timelineId, + }) + ); + } else { + dispatch( + tGridActions.upsertColumn({ + column: getColumnHeader(timelineId, fieldId), + id: timelineId, + index: 1, + }) + ); + } + }, + [columnHeaders, dispatch, timelineId] + ); + + const columns = useMemo( + () => getFieldColumns({ highlight: searchInput, onToggleColumn, getFieldTableColumns }), + [onToggleColumn, searchInput, getFieldTableColumns] + ); + const hasActions = useMemo(() => columns.some((column) => isActionsColumn(column)), [columns]); + + return ( + <> + + {i18n.FIELDS_SHOWING} + {fieldItems.length} + {i18n.FIELDS_COUNT(fieldItems.length)} + + + + + + + ); +}; + +export const FieldTable = React.memo(FieldTableComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.test.tsx deleted file mode 100644 index aec21b48471362..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.test.tsx +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { useMountAppended } from '../../../utils/use_mount_appended'; -import { mockBrowserFields, TestProviders } from '../../../../mock'; - -import { FIELDS_PANE_WIDTH } from './helpers'; -import { FieldsPane } from './fields_pane'; - -const timelineId = 'test'; - -describe('FieldsPane', () => { - const mount = useMountAppended(); - - test('it renders the selected category', () => { - const selectedCategory = 'auditd'; - - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="selected-category-title"]`).first().text()).toEqual( - selectedCategory - ); - }); - - test('it renders a unknown category that does not exist in filteredBrowserFields', () => { - const selectedCategory = 'unknown'; - - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="selected-category-title"]`).first().text()).toEqual( - selectedCategory - ); - }); - - test('it renders the expected message when `filteredBrowserFields` is empty and `searchInput` is empty', () => { - const searchInput = ''; - - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="no-fields-match"]`).first().text()).toEqual( - 'No fields match ' - ); - }); - - test('it renders the expected message when `filteredBrowserFields` is empty and `searchInput` is an unknown field name', () => { - const searchInput = 'thisFieldDoesNotExist'; - - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="no-fields-match"]`).first().text()).toEqual( - `No fields match ${searchInput}` - ); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx deleted file mode 100644 index d1d0254d0c917d..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; -import styled from 'styled-components'; -import { useDispatch } from 'react-redux'; - -import { Category } from './category'; -import { getFieldItems } from './field_items'; -import { FIELDS_PANE_WIDTH, TABLE_HEIGHT } from './helpers'; - -import * as i18n from './translations'; -import type { BrowserFields } from '../../../../../common/search_strategy'; -import type { ColumnHeaderOptions, OnUpdateColumns } from '../../../../../common/types'; -import { tGridActions } from '../../../../store/t_grid'; - -const NoFieldsPanel = styled.div` - background-color: ${(props) => props.theme.eui.euiColorLightestShade}; - width: ${FIELDS_PANE_WIDTH}px; - height: ${TABLE_HEIGHT}px; -`; - -NoFieldsPanel.displayName = 'NoFieldsPanel'; - -const NoFieldsFlexGroup = styled(EuiFlexGroup)` - height: 100%; -`; - -NoFieldsFlexGroup.displayName = 'NoFieldsFlexGroup'; - -interface Props { - timelineId: string; - columnHeaders: ColumnHeaderOptions[]; - /** - * A map of categoryId -> metadata about the fields in that category, - * filtered such that the name of every field in the category includes - * the filter input (as a substring). - */ - filteredBrowserFields: BrowserFields; - /** - * Invoked when the user clicks on the name of a category in the left-hand - * side of the field browser - */ - onCategorySelected: (categoryId: string) => void; - /** The text displayed in the search input */ - /** Invoked when a user chooses to view a new set of columns in the timeline */ - onUpdateColumns: OnUpdateColumns; - searchInput: string; - /** - * The category selected on the left-hand side of the field browser - */ - selectedCategoryId: string; - /** The width field browser */ - width: number; -} -export const FieldsPane = React.memo( - ({ - columnHeaders, - filteredBrowserFields, - onCategorySelected, - onUpdateColumns, - searchInput, - selectedCategoryId, - timelineId, - width, - }) => { - const dispatch = useDispatch(); - - const toggleColumn = useCallback( - (column: ColumnHeaderOptions) => { - if (columnHeaders.some((c) => c.id === column.id)) { - dispatch( - tGridActions.removeColumn({ - columnId: column.id, - id: timelineId, - }) - ); - } else { - dispatch( - tGridActions.upsertColumn({ - column, - id: timelineId, - index: 1, - }) - ); - } - }, - [columnHeaders, dispatch, timelineId] - ); - - const filteredBrowserFieldsExists = useMemo( - () => Object.keys(filteredBrowserFields).length > 0, - [filteredBrowserFields] - ); - - const fieldItems = useMemo(() => { - return getFieldItems({ - category: filteredBrowserFields[selectedCategoryId], - columnHeaders, - highlight: searchInput, - timelineId, - toggleColumn, - }); - }, [ - columnHeaders, - filteredBrowserFields, - searchInput, - selectedCategoryId, - timelineId, - toggleColumn, - ]); - - if (filteredBrowserFieldsExists) { - return ( - - ); - } - - return ( - - - -

{i18n.NO_FIELDS_MATCH_INPUT(searchInput)}

-
-
-
- ); - } -); - -FieldsPane.displayName = 'FieldsPane'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx index 239d7c726e286b..ad90956013e41d 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx @@ -10,45 +10,12 @@ import { mockBrowserFields } from '../../../../mock'; import { categoryHasFields, createVirtualCategory, - getCategoryPaneCategoryClassName, - getFieldBrowserCategoryTitleClassName, - getFieldBrowserSearchInputClassName, getFieldCount, filterBrowserFieldsByFieldName, } from './helpers'; import { BrowserFields } from '../../../../../common/search_strategy'; -const timelineId = 'test'; - describe('helpers', () => { - describe('getCategoryPaneCategoryClassName', () => { - test('it returns the expected class name', () => { - const categoryId = 'auditd'; - - expect(getCategoryPaneCategoryClassName({ categoryId, timelineId })).toEqual( - 'field-browser-category-pane-auditd-test' - ); - }); - }); - - describe('getFieldBrowserCategoryTitleClassName', () => { - test('it returns the expected class name', () => { - const categoryId = 'auditd'; - - expect(getFieldBrowserCategoryTitleClassName({ categoryId, timelineId })).toEqual( - 'field-browser-category-title-auditd-test' - ); - }); - }); - - describe('getFieldBrowserSearchInputClassName', () => { - test('it returns the expected class name', () => { - expect(getFieldBrowserSearchInputClassName(timelineId)).toEqual( - 'field-browser-search-input-test' - ); - }); - }); - describe('categoryHasFields', () => { test('it returns false if the category fields property is undefined', () => { expect(categoryHasFields({})).toBe(false); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx index 5406940aab3e9c..21829bda265e1f 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx @@ -9,11 +9,6 @@ import { EuiBadge, EuiLoadingSpinner } from '@elastic/eui'; import { filter, get, pickBy } from 'lodash/fp'; import styled from 'styled-components'; -import { - elementOrChildrenHasFocus, - skipFocusInContainerTo, - stopPropagationAndPreventDefault, -} from '../../../../../common/utils/accessibility'; import { TimelineId } from '../../../../../public/types'; import type { BrowserField, BrowserFields } from '../../../../../common/search_strategy'; import { defaultHeaders } from '../../../../store/t_grid/defaults'; @@ -27,44 +22,8 @@ export const LoadingSpinner = styled(EuiLoadingSpinner)` LoadingSpinner.displayName = 'LoadingSpinner'; -export const CATEGORY_PANE_WIDTH = 200; -export const DESCRIPTION_COLUMN_WIDTH = 300; -export const FIELD_COLUMN_WIDTH = 200; export const FIELD_BROWSER_WIDTH = 925; -export const FIELDS_PANE_WIDTH = 670; -export const HEADER_HEIGHT = 40; -export const PANES_FLEX_GROUP_WIDTH = CATEGORY_PANE_WIDTH + FIELDS_PANE_WIDTH + 10; -export const PANES_FLEX_GROUP_HEIGHT = 260; export const TABLE_HEIGHT = 260; -export const TYPE_COLUMN_WIDTH = 50; - -/** - * Returns the CSS class name for the title of a category shown in the left - * side field browser - */ -export const getCategoryPaneCategoryClassName = ({ - categoryId, - timelineId, -}: { - categoryId: string; - timelineId: string; -}): string => `field-browser-category-pane-${categoryId}-${timelineId}`; - -/** - * Returns the CSS class name for the title of a category shown in the right - * side of field browser - */ -export const getFieldBrowserCategoryTitleClassName = ({ - categoryId, - timelineId, -}: { - categoryId: string; - timelineId: string; -}): string => `field-browser-category-title-${categoryId}-${timelineId}`; - -/** Returns the class name for a field browser search input */ -export const getFieldBrowserSearchInputClassName = (timelineId: string): string => - `field-browser-search-input-${timelineId}`; /** Returns true if the specified category has at least one field */ export const categoryHasFields = (category: Partial): boolean => @@ -160,272 +119,22 @@ export const getAlertColumnHeader = (timelineId: string, fieldId: string) => ? defaultHeaders.find((c) => c.id === fieldId) ?? {} : {}; -export const CATEGORIES_PANE_CLASS_NAME = 'categories-pane'; export const CATEGORY_TABLE_CLASS_NAME = 'category-table'; export const CLOSE_BUTTON_CLASS_NAME = 'close-button'; export const RESET_FIELDS_CLASS_NAME = 'reset-fields'; -export const VIEW_ALL_BUTTON_CLASS_NAME = 'view-all'; - -export const categoriesPaneHasFocus = (containerElement: HTMLElement | null): boolean => - elementOrChildrenHasFocus( - containerElement?.querySelector(`.${CATEGORIES_PANE_CLASS_NAME}`) - ); - -export const categoryTableHasFocus = (containerElement: HTMLElement | null): boolean => - elementOrChildrenHasFocus( - containerElement?.querySelector(`.${CATEGORY_TABLE_CLASS_NAME}`) - ); - -export const closeButtonHasFocus = (containerElement: HTMLElement | null): boolean => - elementOrChildrenHasFocus( - containerElement?.querySelector(`.${CLOSE_BUTTON_CLASS_NAME}`) - ); - -export const searchInputHasFocus = ({ - containerElement, - timelineId, -}: { - containerElement: HTMLElement | null; - timelineId: string; -}): boolean => - elementOrChildrenHasFocus( - containerElement?.querySelector( - `.${getFieldBrowserSearchInputClassName(timelineId)}` - ) - ); - -export const viewAllHasFocus = (containerElement: HTMLElement | null): boolean => - elementOrChildrenHasFocus( - containerElement?.querySelector(`.${VIEW_ALL_BUTTON_CLASS_NAME}`) - ); - -export const resetButtonHasFocus = (containerElement: HTMLElement | null): boolean => - elementOrChildrenHasFocus( - containerElement?.querySelector(`.${RESET_FIELDS_CLASS_NAME}`) - ); - -export const scrollCategoriesPane = ({ - containerElement, - selectedCategoryId, - timelineId, -}: { - containerElement: HTMLElement | null; - selectedCategoryId: string; - timelineId: string; -}) => { - if (selectedCategoryId !== '') { - const selectedCategories = - containerElement?.getElementsByClassName( - getCategoryPaneCategoryClassName({ - categoryId: selectedCategoryId, - timelineId, - }) - ) ?? []; - - if (selectedCategories.length > 0) { - selectedCategories[0].scrollIntoView(); - } - } -}; - -export const focusCategoriesPane = ({ - containerElement, - selectedCategoryId, - timelineId, -}: { - containerElement: HTMLElement | null; - selectedCategoryId: string; - timelineId: string; -}) => { - if (selectedCategoryId !== '') { - const selectedCategories = - containerElement?.getElementsByClassName( - getCategoryPaneCategoryClassName({ - categoryId: selectedCategoryId, - timelineId, - }) - ) ?? []; - - if (selectedCategories.length > 0) { - (selectedCategories[0] as HTMLButtonElement).focus(); - } - } -}; - -export const focusCategoryTable = (containerElement: HTMLElement | null) => { - const firstEntry = containerElement?.querySelector( - `.${CATEGORY_TABLE_CLASS_NAME} [data-colindex="1"]` - ); - - if (firstEntry != null) { - firstEntry.focus(); - } else { - skipFocusInContainerTo({ - containerElement, - className: CATEGORY_TABLE_CLASS_NAME, - }); - } -}; - -export const focusCloseButton = (containerElement: HTMLElement | null) => - skipFocusInContainerTo({ - containerElement, - className: CLOSE_BUTTON_CLASS_NAME, - }); - -export const focusResetFieldsButton = (containerElement: HTMLElement | null) => - skipFocusInContainerTo({ containerElement, className: RESET_FIELDS_CLASS_NAME }); - -export const focusSearchInput = ({ - containerElement, - timelineId, -}: { - containerElement: HTMLElement | null; - timelineId: string; -}) => - skipFocusInContainerTo({ - containerElement, - className: getFieldBrowserSearchInputClassName(timelineId), - }); - -export const focusViewAllButton = (containerElement: HTMLElement | null) => - skipFocusInContainerTo({ containerElement, className: VIEW_ALL_BUTTON_CLASS_NAME }); - -export const onCategoriesPaneFocusChanging = ({ - containerElement, - shiftKey, - timelineId, -}: { - containerElement: HTMLElement | null; - shiftKey: boolean; - timelineId: string; -}) => - shiftKey - ? focusSearchInput({ - containerElement, - timelineId, - }) - : focusViewAllButton(containerElement); - -export const onCategoryTableFocusChanging = ({ - containerElement, - shiftKey, -}: { - containerElement: HTMLElement | null; - shiftKey: boolean; -}) => (shiftKey ? focusViewAllButton(containerElement) : focusResetFieldsButton(containerElement)); - -export const onCloseButtonFocusChanging = ({ - containerElement, - shiftKey, - timelineId, -}: { - containerElement: HTMLElement | null; - shiftKey: boolean; - timelineId: string; -}) => - shiftKey - ? focusResetFieldsButton(containerElement) - : focusSearchInput({ containerElement, timelineId }); - -export const onSearchInputFocusChanging = ({ - containerElement, - selectedCategoryId, - shiftKey, - timelineId, -}: { - containerElement: HTMLElement | null; - selectedCategoryId: string; - shiftKey: boolean; - timelineId: string; -}) => - shiftKey - ? focusCloseButton(containerElement) - : focusCategoriesPane({ containerElement, selectedCategoryId, timelineId }); - -export const onViewAllFocusChanging = ({ - containerElement, - selectedCategoryId, - shiftKey, - timelineId, -}: { - containerElement: HTMLElement | null; - selectedCategoryId: string; - shiftKey: boolean; - timelineId: string; -}) => - shiftKey - ? focusCategoriesPane({ containerElement, selectedCategoryId, timelineId }) - : focusCategoryTable(containerElement); - -export const onResetButtonFocusChanging = ({ - containerElement, - shiftKey, -}: { - containerElement: HTMLElement | null; - shiftKey: boolean; -}) => (shiftKey ? focusCategoryTable(containerElement) : focusCloseButton(containerElement)); - -export const onFieldsBrowserTabPressed = ({ - containerElement, - keyboardEvent, - selectedCategoryId, - timelineId, -}: { - containerElement: HTMLElement | null; - keyboardEvent: React.KeyboardEvent; - selectedCategoryId: string; - timelineId: string; -}) => { - const { shiftKey } = keyboardEvent; - - if (searchInputHasFocus({ containerElement, timelineId })) { - stopPropagationAndPreventDefault(keyboardEvent); - onSearchInputFocusChanging({ - containerElement, - selectedCategoryId, - shiftKey, - timelineId, - }); - } else if (categoriesPaneHasFocus(containerElement)) { - stopPropagationAndPreventDefault(keyboardEvent); - onCategoriesPaneFocusChanging({ - containerElement, - shiftKey, - timelineId, - }); - } else if (viewAllHasFocus(containerElement)) { - stopPropagationAndPreventDefault(keyboardEvent); - onViewAllFocusChanging({ - containerElement, - selectedCategoryId, - shiftKey, - timelineId, - }); - } else if (categoryTableHasFocus(containerElement)) { - stopPropagationAndPreventDefault(keyboardEvent); - onCategoryTableFocusChanging({ - containerElement, - shiftKey, - }); - } else if (resetButtonHasFocus(containerElement)) { - stopPropagationAndPreventDefault(keyboardEvent); - onResetButtonFocusChanging({ - containerElement, - shiftKey, - }); - } else if (closeButtonHasFocus(containerElement)) { - stopPropagationAndPreventDefault(keyboardEvent); - onCloseButtonFocusChanging({ - containerElement, - shiftKey, - timelineId, - }); - } -}; export const CountBadge = styled(EuiBadge)` margin-left: 5px; ` as unknown as typeof EuiBadge; CountBadge.displayName = 'CountBadge'; + +export const CategoryName = styled.span<{ bold: boolean }>` + font-weight: ${({ bold }) => (bold ? 'bold' : 'normal')}; +`; +CategoryName.displayName = 'CategoryName'; + +export const CategorySelectableContainer = styled.div` + width: 300px; +`; +CategorySelectableContainer.displayName = 'CategorySelectableContainer'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.test.tsx index b8bc2a12ffd6e8..7db742fd11302c 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.test.tsx @@ -5,9 +5,8 @@ * 2.0. */ -import { mount } from 'enzyme'; import React from 'react'; -import { waitFor } from '@testing-library/react'; +import { act, fireEvent, render, waitFor } from '@testing-library/react'; import { mockBrowserFields, TestProviders } from '../../../../mock'; @@ -18,12 +17,8 @@ import { StatefulFieldsBrowserComponent } from '.'; describe('StatefulFieldsBrowser', () => { const timelineId = 'test'; - beforeEach(() => { - window.HTMLElement.prototype.scrollIntoView = jest.fn(); - }); - - test('it renders the Fields button, which displays the fields browser on click', () => { - const wrapper = mount( + it('should render the Fields button, which displays the fields browser on click', () => { + const result = render( { ); - expect(wrapper.find('[data-test-subj="show-field-browser"]').exists()).toBe(true); + expect(result.getByTestId('show-field-browser')).toBeInTheDocument(); }); describe('toggleShow', () => { - test('it does NOT render the fields browser until the Fields button is clicked', () => { - const wrapper = mount( + it('should NOT render the fields browser until the Fields button is clicked', () => { + const result = render( { ); - expect(wrapper.find('[data-test-subj="fields-browser-container"]').exists()).toBe(false); + expect(result.queryByTestId('fields-browser-container')).toBeNull(); }); - test('it renders the fields browser when the Fields button is clicked', () => { - const wrapper = mount( + it('should render the fields browser when the Fields button is clicked', async () => { + const result = render( { /> ); - - wrapper.find('[data-test-subj="show-field-browser"]').first().simulate('click'); - - expect(wrapper.find('[data-test-subj="fields-browser-container"]').exists()).toBe(true); + result.getByTestId('show-field-browser').click(); + await waitFor(() => { + expect(result.getByTestId('fields-browser-container')).toBeInTheDocument(); + }); }); }); - describe('updateSelectedCategoryId', () => { - beforeEach(() => { - jest.useFakeTimers(); - }); - test('it updates the selectedCategoryId state, which makes the category bold, when the user clicks a category name in the left hand side of the field browser', async () => { - const wrapper = mount( + describe('updateSelectedCategoryIds', () => { + it('should add a selected category, which creates the category badge', async () => { + const result = render( { ); - wrapper.find('[data-test-subj="show-field-browser"]').first().simulate('click'); + result.getByTestId('show-field-browser').click(); + await waitFor(() => { + expect(result.getByTestId('fields-browser-container')).toBeInTheDocument(); + }); + + await act(async () => { + result.getByTestId('categories-filter-button').click(); + }); + await act(async () => { + result.getByTestId('categories-selector-option-base').click(); + }); + + expect(result.getByTestId('category-badge-base')).toBeInTheDocument(); + }); + + it('should remove a selected category, which deletes the category badge', async () => { + const result = render( + + + + ); - wrapper.find(`.field-browser-category-pane-auditd-${timelineId}`).first().simulate('click'); + result.getByTestId('show-field-browser').click(); await waitFor(() => { - wrapper.update(); - expect( - wrapper - .find(`.field-browser-category-pane-auditd-${timelineId}`) - .find('[data-test-subj="categoryName"]') - .at(1) - ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); + expect(result.getByTestId('fields-browser-container')).toBeInTheDocument(); }); + + await act(async () => { + result.getByTestId('categories-filter-button').click(); + }); + await act(async () => { + result.getByTestId('categories-selector-option-base').click(); + }); + expect(result.getByTestId('category-badge-base')).toBeInTheDocument(); + + await act(async () => { + result.getByTestId('category-badge-unselect-base').click(); + }); + expect(result.queryByTestId('category-badge-base')).toBeNull(); }); - test('it updates the selectedCategoryId state according to most fields returned', async () => { - const wrapper = mount( + it('should update the available categories according to the search input', async () => { + const result = render( { ); + result.getByTestId('show-field-browser').click(); await waitFor(() => { - wrapper.find('[data-test-subj="show-field-browser"]').first().simulate('click'); - jest.runOnlyPendingTimers(); - wrapper.update(); - - expect( - wrapper - .find(`.field-browser-category-pane-cloud-${timelineId}`) - .find('[data-test-subj="categoryName"]') - .at(1) - ).toHaveStyleRule('font-weight', 'normal', { modifier: '.euiText' }); + expect(result.getByTestId('fields-browser-container')).toBeInTheDocument(); }); + result.getByTestId('categories-filter-button').click(); + expect(result.getByTestId('categories-selector-option-base')).toBeInTheDocument(); + + fireEvent.change(result.getByTestId('field-search'), { target: { value: 'client' } }); await waitFor(() => { - wrapper - .find('[data-test-subj="field-search"]') - .last() - .simulate('change', { target: { value: 'cloud' } }); - - jest.runOnlyPendingTimers(); - wrapper.update(); - expect( - wrapper - .find(`.field-browser-category-pane-cloud-${timelineId}`) - .find('[data-test-subj="categoryName"]') - .at(1) - ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); + expect(result.queryByTestId('categories-selector-option-base')).toBeNull(); }); + expect(result.queryByTestId('categories-selector-option-client')).toBeInTheDocument(); }); }); - test('it renders the Fields Browser button as a settings gear when the isEventViewer prop is true', () => { + it('should render the Fields Browser button as a settings gear when the isEventViewer prop is true', () => { const isEventViewer = true; - const wrapper = mount( + const result = render( { ); - expect(wrapper.find('[data-test-subj="show-field-browser"]').first().exists()).toBe(true); + expect(result.getByTestId('show-field-browser')).toBeInTheDocument(); }); - test('it renders the Fields Browser button as a settings gear when the isEventViewer prop is false', () => { - const isEventViewer = true; + it('should render the Fields Browser button as a settings gear when the isEventViewer prop is false', () => { + const isEventViewer = false; - const wrapper = mount( + const result = render( { ); - expect(wrapper.find('[data-test-subj="show-field-browser"]').first().exists()).toBe(true); + expect(result.getByTestId('show-field-browser')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.tsx index 13549e2d5be109..c5647c973b9d8c 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.tsx @@ -6,15 +6,15 @@ */ import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; +import { debounce } from 'lodash'; import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react'; import styled from 'styled-components'; import type { BrowserFields } from '../../../../../common/search_strategy/index_fields'; -import { DEFAULT_CATEGORY_NAME } from '../../body/column_headers/default_headers'; +import type { FieldBrowserProps } from '../../../../../common/types/fields_browser'; import { FieldsBrowser } from './field_browser'; import { filterBrowserFieldsByFieldName, mergeBrowserFieldsWithDefaultCategory } from './helpers'; import * as i18n from './translations'; -import type { FieldBrowserProps } from './types'; const FIELDS_BUTTON_CLASS_NAME = 'fields-button'; @@ -34,26 +34,48 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ timelineId, columnHeaders, browserFields, - createFieldComponent, + options, width, }) => { const customizeColumnsButtonRef = useRef(null); - /** tracks the latest timeout id from `setTimeout`*/ - const inputTimeoutId = useRef(0); - /** all field names shown in the field browser must contain this string (when specified) */ const [filterInput, setFilterInput] = useState(''); - + /** debounced filterInput, the one that is applied to the filteredBrowserFields */ const [appliedFilterInput, setAppliedFilterInput] = useState(''); /** all fields in this collection have field names that match the filterInput */ const [filteredBrowserFields, setFilteredBrowserFields] = useState(null); /** when true, show a spinner in the input to indicate the field browser is searching for matching field names */ const [isSearching, setIsSearching] = useState(false); /** this category will be displayed in the right-hand pane of the field browser */ - const [selectedCategoryId, setSelectedCategoryId] = useState(DEFAULT_CATEGORY_NAME); + const [selectedCategoryIds, setSelectedCategoryIds] = useState([]); /** show the field browser */ const [show, setShow] = useState(false); + // debounced function to apply the input filter + // will delay the call to setAppliedFilterInput by INPUT_TIMEOUT ms + // the parameter used will be the last one passed + const debouncedApplyFilterInput = useMemo( + () => + debounce((input: string) => { + setAppliedFilterInput(input); + }, INPUT_TIMEOUT), + [] + ); + useEffect(() => { + return () => { + debouncedApplyFilterInput.cancel(); + }; + }, [debouncedApplyFilterInput]); + + useEffect(() => { + const newFilteredBrowserFields = filterBrowserFieldsByFieldName({ + browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields), + substring: appliedFilterInput, + }); + setFilteredBrowserFields(newFilteredBrowserFields); + setIsSearching(false); + }, [appliedFilterInput, browserFields]); + /** Shows / hides the field browser */ const onShow = useCallback(() => { setShow(true); @@ -65,65 +87,19 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ setAppliedFilterInput(''); setFilteredBrowserFields(null); setIsSearching(false); - setSelectedCategoryId(DEFAULT_CATEGORY_NAME); + setSelectedCategoryIds([]); setShow(false); }, []); - const newFilteredBrowserFields = useMemo(() => { - return filterBrowserFieldsByFieldName({ - browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields), - substring: appliedFilterInput, - }); - }, [appliedFilterInput, browserFields]); - - const newSelectedCategoryId = useMemo(() => { - if (appliedFilterInput === '' || Object.keys(newFilteredBrowserFields).length === 0) { - return DEFAULT_CATEGORY_NAME; - } else { - return Object.keys(newFilteredBrowserFields) - .sort() - .reduce((selected, category) => { - const filteredBrowserFieldsByCategory = - (newFilteredBrowserFields[category] && newFilteredBrowserFields[category].fields) || []; - const filteredBrowserFieldsBySelected = - (newFilteredBrowserFields[selected] && newFilteredBrowserFields[selected].fields) || []; - return newFilteredBrowserFields[category].fields != null && - newFilteredBrowserFields[selected].fields != null && - Object.keys(filteredBrowserFieldsByCategory).length > - Object.keys(filteredBrowserFieldsBySelected).length - ? category - : selected; - }, Object.keys(newFilteredBrowserFields)[0]); - } - }, [appliedFilterInput, newFilteredBrowserFields]); - /** Invoked when the user types in the filter input */ - const updateFilter = useCallback((newFilterInput: string) => { - setFilterInput(newFilterInput); - setIsSearching(true); - }, []); - - useEffect(() => { - if (inputTimeoutId.current !== 0) { - clearTimeout(inputTimeoutId.current); // ⚠️ mutation: cancel any previous timers - } - // ⚠️ mutation: schedule a new timer that will apply the filter when it fires: - inputTimeoutId.current = window.setTimeout(() => { - setIsSearching(false); - setAppliedFilterInput(filterInput); - }, INPUT_TIMEOUT); - return () => { - clearTimeout(inputTimeoutId.current); - }; - }, [filterInput]); - - useEffect(() => { - setFilteredBrowserFields(newFilteredBrowserFields); - }, [newFilteredBrowserFields]); - - useEffect(() => { - setSelectedCategoryId(newSelectedCategoryId); - }, [newSelectedCategoryId]); + const updateFilter = useCallback( + (newFilterInput: string) => { + setIsSearching(true); + setFilterInput(newFilterInput); + debouncedApplyFilterInput(newFilterInput); + }, + [debouncedApplyFilterInput] + ); // only merge in the default category if the field browser is visible const browserFieldsWithDefaultCategory = useMemo(() => { @@ -150,19 +126,19 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ {show && ( diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.test.tsx index f5668b1bdc08d4..fb6363e2444592 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.test.tsx @@ -7,7 +7,7 @@ import { mount } from 'enzyme'; import React from 'react'; -import { mockBrowserFields, TestProviders } from '../../../../mock'; +import { TestProviders } from '../../../../mock'; import { Search } from './search'; const timelineId = 'test'; @@ -17,7 +17,6 @@ describe('Search', () => { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { expect(onSearchInputChange).toBeCalled(); }); - - test('it returns the expected categories count when filteredBrowserFields is empty', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="categories-count"]').first().text()).toEqual( - '0 categories' - ); - }); - - test('it returns the expected categories count when filteredBrowserFields is NOT empty', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="categories-count"]').first().text()).toEqual( - '12 categories' - ); - }); - - test('it returns the expected fields count when filteredBrowserFields is empty', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="fields-count"]').first().text()).toEqual('0 fields'); - }); - - test('it returns the expected fields count when filteredBrowserFields is NOT empty', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="fields-count"]').first().text()).toEqual('34 fields'); - }); }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.tsx index 935952fbf37e00..037dcdc9033d2f 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.tsx @@ -6,75 +6,28 @@ */ import React from 'react'; -import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import styled from 'styled-components'; -import type { BrowserFields } from '../../../../../common/search_strategy'; - -import { getFieldBrowserSearchInputClassName, getFieldCount } from './helpers'; - +import { EuiFieldSearch } from '@elastic/eui'; import * as i18n from './translations'; - -const CountsFlexGroup = styled(EuiFlexGroup)` - margin-top: ${({ theme }) => theme.eui.euiSizeXS}; - margin-left: ${({ theme }) => theme.eui.euiSizeXS}; -`; - -CountsFlexGroup.displayName = 'CountsFlexGroup'; - interface Props { - filteredBrowserFields: BrowserFields; isSearching: boolean; onSearchInputChange: (event: React.ChangeEvent) => void; searchInput: string; timelineId: string; } -const CountRow = React.memo>(({ filteredBrowserFields }) => ( - - - - {i18n.CATEGORIES_COUNT(Object.keys(filteredBrowserFields).length)} - - - - - - {i18n.FIELDS_COUNT( - Object.keys(filteredBrowserFields).reduce( - (fieldsCount, category) => getFieldCount(filteredBrowserFields[category]) + fieldsCount, - 0 - ) - )} - - - -)); - -CountRow.displayName = 'CountRow'; - const inputRef = (node: HTMLInputElement | null) => node?.focus(); export const Search = React.memo( - ({ isSearching, filteredBrowserFields, onSearchInputChange, searchInput, timelineId }) => ( - <> - - - + ({ isSearching, onSearchInputChange, searchInput, timelineId }) => ( + ) ); - Search.displayName = 'Search'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/translations.ts index ac0160fad6cdee..eab412971c580c 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/translations.ts +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/translations.ts @@ -21,21 +21,6 @@ export const CATEGORIES_COUNT = (totalCount: number) => defaultMessage: '{totalCount} {totalCount, plural, =1 {category} other {categories}}', }); -export const CATEGORY_LINK = ({ category, totalCount }: { category: string; totalCount: number }) => - i18n.translate('xpack.timelines.fieldBrowser.categoryLinkAriaLabel', { - values: { category, totalCount }, - defaultMessage: - '{category} {totalCount} {totalCount, plural, =1 {field} other {fields}}. Click this button to select the {category} category.', - }); - -export const CATEGORY_FIELDS_TABLE_CAPTION = (categoryId: string) => - i18n.translate('xpack.timelines.fieldBrowser.categoryFieldsTableCaption', { - defaultMessage: 'category {categoryId} fields', - values: { - categoryId, - }, - }); - export const CLOSE = i18n.translate('xpack.timelines.fieldBrowser.closeButton', { defaultMessage: 'Close', }); @@ -56,6 +41,10 @@ export const DESCRIPTION_FOR_FIELD = (field: string) => defaultMessage: 'Description for field {field}:', }); +export const NAME = i18n.translate('xpack.timelines.fieldBrowser.fieldName', { + defaultMessage: 'Name', +}); + export const FIELD = i18n.translate('xpack.timelines.fieldBrowser.fieldLabel', { defaultMessage: 'Field', }); @@ -64,10 +53,14 @@ export const FIELDS = i18n.translate('xpack.timelines.fieldBrowser.fieldsTitle', defaultMessage: 'Fields', }); +export const FIELDS_SHOWING = i18n.translate('xpack.timelines.fieldBrowser.fieldsCountShowing', { + defaultMessage: 'Showing', +}); + export const FIELDS_COUNT = (totalCount: number) => i18n.translate('xpack.timelines.fieldBrowser.fieldsCountTitle', { values: { totalCount }, - defaultMessage: '{totalCount} {totalCount, plural, =1 {field} other {fields}}', + defaultMessage: '{totalCount, plural, =1 {field} other {fields}}', }); export const FILTER_PLACEHOLDER = i18n.translate('xpack.timelines.fieldBrowser.filterPlaceholder', { @@ -90,14 +83,6 @@ export const RESET_FIELDS = i18n.translate('xpack.timelines.fieldBrowser.resetFi defaultMessage: 'Reset Fields', }); -export const VIEW_ALL_CATEGORY_FIELDS = (categoryId: string) => - i18n.translate('xpack.timelines.fieldBrowser.viewCategoryTooltip', { - defaultMessage: 'View all {categoryId} fields', - values: { - categoryId, - }, - }); - export const VIEW_COLUMN = (field: string) => i18n.translate('xpack.timelines.fieldBrowser.viewColumnCheckboxAriaLabel', { values: { field }, diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/types.ts b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/types.ts deleted file mode 100644 index bcf7287950624f..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/types.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CreateFieldComponentType } from '../../../../../common/types'; -import type { BrowserFields } from '../../../../../common/search_strategy/index_fields'; -import type { ColumnHeaderOptions } from '../../../../../common/types/timeline/columns'; - -export type OnFieldSelected = (fieldId: string) => void; - -export interface FieldBrowserProps { - /** The timeline associated with this field browser */ - timelineId: string; - /** The timeline's current column headers */ - columnHeaders: ColumnHeaderOptions[]; - /** A map of categoryId -> metadata about the fields in that category */ - browserFields: BrowserFields; - - createFieldComponent?: CreateFieldComponentType; - /** When true, this Fields Browser is being used as an "events viewer" */ - isEventViewer?: boolean; - /** The width of the field browser */ - width?: number; -} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 70d65550a40563..c35b5dbe666782 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -27035,9 +27035,7 @@ "xpack.timelines.exitFullScreenButton": "全画面を終了", "xpack.timelines.fieldBrowser.categoriesCountTitle": "{totalCount} {totalCount, plural, other {カテゴリ}}", "xpack.timelines.fieldBrowser.categoriesTitle": "カテゴリー", - "xpack.timelines.fieldBrowser.categoryFieldsTableCaption": "カテゴリ {categoryId} フィールド", "xpack.timelines.fieldBrowser.categoryLabel": "カテゴリー", - "xpack.timelines.fieldBrowser.categoryLinkAriaLabel": "{category} {totalCount} {totalCount, plural, other {フィールド}}このボタンをクリックすると、{category} カテゴリを選択します。", "xpack.timelines.fieldBrowser.closeButton": "閉じる", "xpack.timelines.fieldBrowser.descriptionForScreenReaderOnly": "フィールド {field} の説明:", "xpack.timelines.fieldBrowser.descriptionLabel": "説明", @@ -27049,7 +27047,6 @@ "xpack.timelines.fieldBrowser.noFieldsMatchInputLabel": "{searchInput} に一致するフィールドがありません", "xpack.timelines.fieldBrowser.noFieldsMatchLabel": "一致するフィールドがありません", "xpack.timelines.fieldBrowser.resetFieldsLink": "フィールドをリセット", - "xpack.timelines.fieldBrowser.viewCategoryTooltip": "すべての {categoryId} フィールドを表示します", "xpack.timelines.fieldBrowser.viewColumnCheckboxAriaLabel": "{field} 列を表示", "xpack.timelines.footer.autoRefreshActiveDescription": "自動更新アクション", "xpack.timelines.footer.autoRefreshActiveTooltip": "自動更新が有効な間、タイムラインはクエリに一致する最新の {numberOfItems} 件のイベントを表示します。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 90eb0d3c35f653..f706762740ad8e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -27067,9 +27067,7 @@ "xpack.timelines.exitFullScreenButton": "退出全屏", "xpack.timelines.fieldBrowser.categoriesCountTitle": "{totalCount} {totalCount, plural, other {个类别}}", "xpack.timelines.fieldBrowser.categoriesTitle": "类别", - "xpack.timelines.fieldBrowser.categoryFieldsTableCaption": "类别 {categoryId} 字段", "xpack.timelines.fieldBrowser.categoryLabel": "类别", - "xpack.timelines.fieldBrowser.categoryLinkAriaLabel": "{category} {totalCount} 个{totalCount, plural, other {字段}}。单击此按钮可选择 {category} 类别。", "xpack.timelines.fieldBrowser.closeButton": "关闭", "xpack.timelines.fieldBrowser.descriptionForScreenReaderOnly": "{field} 字段的描述:", "xpack.timelines.fieldBrowser.descriptionLabel": "描述", @@ -27081,7 +27079,6 @@ "xpack.timelines.fieldBrowser.noFieldsMatchInputLabel": "没有字段匹配“{searchInput}”", "xpack.timelines.fieldBrowser.noFieldsMatchLabel": "没有字段匹配", "xpack.timelines.fieldBrowser.resetFieldsLink": "重置字段", - "xpack.timelines.fieldBrowser.viewCategoryTooltip": "查看所有 {categoryId} 字段", "xpack.timelines.fieldBrowser.viewColumnCheckboxAriaLabel": "查看 {field} 列", "xpack.timelines.footer.autoRefreshActiveDescription": "自动刷新已启用", "xpack.timelines.footer.autoRefreshActiveTooltip": "自动刷新已启用时,时间线将显示匹配查询的最近 {numberOfItems} 个事件。", From eca203ce73500e5ff336656e00a8eb658ec7af20 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 7 Mar 2022 16:10:41 +0100 Subject: [PATCH 12/19] [AggConfigs] Add TopMetrics agg (#125936) --- packages/kbn-doc-links/src/get_doc_links.ts | 1 + .../data/common/search/aggs/agg_types.ts | 2 + .../common/search/aggs/aggs_service.test.ts | 2 + .../data/common/search/aggs/metrics/index.ts | 2 + .../metrics/lib/parent_pipeline_agg_helper.ts | 1 + .../lib/sibling_pipeline_agg_helper.ts | 1 + .../search/aggs/metrics/metric_agg_type.ts | 1 + .../search/aggs/metrics/metric_agg_types.ts | 1 + .../search/aggs/metrics/top_metrics.test.ts | 194 ++++++++++++++++++ .../common/search/aggs/metrics/top_metrics.ts | 155 ++++++++++++++ .../aggs/metrics/top_metrics_fn.test.ts | 79 +++++++ .../search/aggs/metrics/top_metrics_fn.ts | 106 ++++++++++ .../common/search/aggs/param_types/field.ts | 1 + src/plugins/data/common/search/aggs/types.ts | 4 + .../public/search/aggs/aggs_service.test.ts | 4 +- .../public/components/agg_params_map.ts | 5 + .../public/components/controls/metric_agg.tsx | 9 +- .../run_pipeline/esaggs_topmetrics.ts | 112 ++++++++++ .../test_suites/run_pipeline/index.ts | 2 + 19 files changed, 679 insertions(+), 3 deletions(-) create mode 100644 src/plugins/data/common/search/aggs/metrics/top_metrics.test.ts create mode 100644 src/plugins/data/common/search/aggs/metrics/top_metrics.ts create mode 100644 src/plugins/data/common/search/aggs/metrics/top_metrics_fn.test.ts create mode 100644 src/plugins/data/common/search/aggs/metrics/top_metrics_fn.ts create mode 100644 test/interpreter_functional/test_suites/run_pipeline/esaggs_topmetrics.ts diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index d73760b280d496..03948af6379104 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -196,6 +196,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { std_dev: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-extendedstats-aggregation.html`, sum: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-sum-aggregation.html`, top_hits: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-top-hits-aggregation.html`, + top_metrics: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-top-metrics.html`, }, runtimeFields: { overview: `${ELASTICSEARCH_DOCS}runtime.html`, diff --git a/src/plugins/data/common/search/aggs/agg_types.ts b/src/plugins/data/common/search/aggs/agg_types.ts index 01ccd401c07acc..d7750c48016cd5 100644 --- a/src/plugins/data/common/search/aggs/agg_types.ts +++ b/src/plugins/data/common/search/aggs/agg_types.ts @@ -36,6 +36,7 @@ export const getAggTypes = () => ({ { name: METRIC_TYPES.PERCENTILES, fn: metrics.getPercentilesMetricAgg }, { name: METRIC_TYPES.PERCENTILE_RANKS, fn: metrics.getPercentileRanksMetricAgg }, { name: METRIC_TYPES.TOP_HITS, fn: metrics.getTopHitMetricAgg }, + { name: METRIC_TYPES.TOP_METRICS, fn: metrics.getTopMetricsMetricAgg }, { name: METRIC_TYPES.DERIVATIVE, fn: metrics.getDerivativeMetricAgg }, { name: METRIC_TYPES.CUMULATIVE_SUM, fn: metrics.getCumulativeSumMetricAgg }, { name: METRIC_TYPES.MOVING_FN, fn: metrics.getMovingAvgMetricAgg }, @@ -109,4 +110,5 @@ export const getAggTypesFunctions = () => [ metrics.aggStdDeviation, metrics.aggSum, metrics.aggTopHit, + metrics.aggTopMetrics, ]; diff --git a/src/plugins/data/common/search/aggs/aggs_service.test.ts b/src/plugins/data/common/search/aggs/aggs_service.test.ts index b7237c7b801342..6090e965489e74 100644 --- a/src/plugins/data/common/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/common/search/aggs/aggs_service.test.ts @@ -95,6 +95,7 @@ describe('Aggs service', () => { "percentiles", "percentile_ranks", "top_hits", + "top_metrics", "derivative", "cumulative_sum", "moving_avg", @@ -147,6 +148,7 @@ describe('Aggs service', () => { "percentiles", "percentile_ranks", "top_hits", + "top_metrics", "derivative", "cumulative_sum", "moving_avg", diff --git a/src/plugins/data/common/search/aggs/metrics/index.ts b/src/plugins/data/common/search/aggs/metrics/index.ts index d37b74a1a28aef..4d80e363251001 100644 --- a/src/plugins/data/common/search/aggs/metrics/index.ts +++ b/src/plugins/data/common/search/aggs/metrics/index.ts @@ -56,3 +56,5 @@ export * from './sum_fn'; export * from './sum'; export * from './top_hit_fn'; export * from './top_hit'; +export * from './top_metrics'; +export * from './top_metrics_fn'; diff --git a/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts b/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts index 478b8309272e39..1fe703313218d8 100644 --- a/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts +++ b/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts @@ -15,6 +15,7 @@ import { parentPipelineAggWriter } from './parent_pipeline_agg_writer'; const metricAggFilter = [ '!top_hits', + '!top_metrics', '!percentiles', '!percentile_ranks', '!median', diff --git a/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts b/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts index f8c903b8cfe420..243a119847a2cf 100644 --- a/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts +++ b/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts @@ -13,6 +13,7 @@ import { IMetricAggConfig, MetricAggParam } from '../metric_agg_type'; const metricAggFilter: string[] = [ '!top_hits', + '!top_metrics', '!percentiles', '!percentile_ranks', '!median', diff --git a/src/plugins/data/common/search/aggs/metrics/metric_agg_type.ts b/src/plugins/data/common/search/aggs/metrics/metric_agg_type.ts index 6ddb0fdd9410d4..5237c1ecffe584 100644 --- a/src/plugins/data/common/search/aggs/metrics/metric_agg_type.ts +++ b/src/plugins/data/common/search/aggs/metrics/metric_agg_type.ts @@ -22,6 +22,7 @@ export interface MetricAggParam extends AggParamType { filterFieldTypes?: FieldTypes; onlyAggregatable?: boolean; + scriptable?: boolean; } const metricType = 'metrics'; diff --git a/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts b/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts index a308153b3816b7..eed6d0a378fc2d 100644 --- a/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts +++ b/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts @@ -27,6 +27,7 @@ export enum METRIC_TYPES { SERIAL_DIFF = 'serial_diff', SUM = 'sum', TOP_HITS = 'top_hits', + TOP_METRICS = 'top_metrics', PERCENTILES = 'percentiles', PERCENTILE_RANKS = 'percentile_ranks', STD_DEV = 'std_dev', diff --git a/src/plugins/data/common/search/aggs/metrics/top_metrics.test.ts b/src/plugins/data/common/search/aggs/metrics/top_metrics.test.ts new file mode 100644 index 00000000000000..9bf5f581aa0a4a --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/top_metrics.test.ts @@ -0,0 +1,194 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getTopMetricsMetricAgg } from './top_metrics'; +import { AggConfigs } from '../agg_configs'; +import { mockAggTypesRegistry } from '../test_helpers'; +import { IMetricAggConfig } from './metric_agg_type'; +import { KBN_FIELD_TYPES } from '../../../../common'; + +describe('Top metrics metric', () => { + let aggConfig: IMetricAggConfig; + + const init = ({ + fieldName = 'field', + fieldType = KBN_FIELD_TYPES.NUMBER, + sortFieldName = 'sortField', + sortFieldType = KBN_FIELD_TYPES.NUMBER, + sortOrder = 'desc', + size = 1, + }: any) => { + const typesRegistry = mockAggTypesRegistry(); + const field = { + name: fieldName, + displayName: fieldName, + type: fieldType, + }; + + const sortField = { + name: sortFieldName, + displayName: sortFieldName, + type: sortFieldType, + }; + + const params = { + size, + field: field.name, + sortField: sortField.name, + sortOrder: { + value: sortOrder, + }, + }; + + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: (name: string) => { + if (name === sortFieldName) return sortField; + if (name === fieldName) return field; + return null; + }, + filter: () => [field, sortField], + }, + } as any; + + const aggConfigs = new AggConfigs( + indexPattern, + [ + { + id: '1', + type: 'top_metrics', + schema: 'metric', + params, + }, + ], + { typesRegistry } + ); + + // Grab the aggConfig off the vis (we don't actually use the vis for anything else) + aggConfig = aggConfigs.aggs[0] as IMetricAggConfig; + }; + + it('should return a label prefixed with Last if sorting in descending order', () => { + init({ fieldName: 'bytes', sortFieldName: '@timestamp' }); + expect(getTopMetricsMetricAgg().makeLabel(aggConfig)).toEqual( + 'Last "bytes" value by "@timestamp"' + ); + }); + + it('should return a label prefixed with First if sorting in ascending order', () => { + init({ + fieldName: 'bytes', + sortFieldName: '@timestamp', + sortOrder: 'asc', + }); + expect(getTopMetricsMetricAgg().makeLabel(aggConfig)).toEqual( + 'First "bytes" value by "@timestamp"' + ); + }); + + it('should return a label with size if larger then 1', () => { + init({ + fieldName: 'bytes', + sortFieldName: '@timestamp', + sortOrder: 'asc', + size: 3, + }); + expect(getTopMetricsMetricAgg().makeLabel(aggConfig)).toEqual( + 'First 3 "bytes" values by "@timestamp"' + ); + }); + + it('should return a fieldName in getValueBucketPath', () => { + init({ + fieldName: 'bytes', + sortFieldName: '@timestamp', + sortOrder: 'asc', + size: 3, + }); + expect(getTopMetricsMetricAgg().getValueBucketPath(aggConfig)).toEqual('1[bytes]'); + }); + + it('produces the expected expression ast', () => { + init({ fieldName: 'machine.os', sortFieldName: '@timestamp' }); + expect(aggConfig.toExpressionAst()).toMatchInlineSnapshot(` + Object { + "chain": Array [ + Object { + "arguments": Object { + "enabled": Array [ + true, + ], + "field": Array [ + "machine.os", + ], + "id": Array [ + "1", + ], + "schema": Array [ + "metric", + ], + "size": Array [ + 1, + ], + "sortField": Array [ + "@timestamp", + ], + "sortOrder": Array [ + "desc", + ], + }, + "function": "aggTopMetrics", + "type": "function", + }, + ], + "type": "expression", + } + `); + }); + + describe('gets value from top metrics bucket', () => { + it('should return null if there is no hits', () => { + const bucket = { + '1': { + top: [], + }, + }; + + init({ fieldName: 'bytes' }); + expect(getTopMetricsMetricAgg().getValue(aggConfig, bucket)).toBe(null); + }); + + it('should return a single value if there is a single hit', () => { + const bucket = { + '1': { + top: [{ sort: [3], metrics: { bytes: 1024 } }], + }, + }; + + init({ fieldName: 'bytes' }); + expect(getTopMetricsMetricAgg().getValue(aggConfig, bucket)).toBe(1024); + }); + + it('should return an array of values if there is a multiple results', () => { + const bucket = { + '1': { + top: [ + { sort: [3], metrics: { bytes: 1024 } }, + { sort: [2], metrics: { bytes: 512 } }, + { sort: [1], metrics: { bytes: 256 } }, + ], + }, + }; + + init({ fieldName: 'bytes' }); + expect(getTopMetricsMetricAgg().getValue(aggConfig, bucket)).toEqual([1024, 512, 256]); + }); + }); +}); diff --git a/src/plugins/data/common/search/aggs/metrics/top_metrics.ts b/src/plugins/data/common/search/aggs/metrics/top_metrics.ts new file mode 100644 index 00000000000000..2079925e0435b1 --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/top_metrics.ts @@ -0,0 +1,155 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import _ from 'lodash'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { i18n } from '@kbn/i18n'; +import { aggTopMetricsFnName } from './top_metrics_fn'; +import { IMetricAggConfig, MetricAggType } from './metric_agg_type'; +import { METRIC_TYPES } from './metric_agg_types'; +import { KBN_FIELD_TYPES } from '../../../../common'; +import { BaseAggParams } from '../types'; + +export interface AggParamsTopMetrics extends BaseAggParams { + field: string; + sortField?: string; + sortOrder?: 'desc' | 'asc'; + size?: number; +} + +export const getTopMetricsMetricAgg = () => { + return new MetricAggType({ + name: METRIC_TYPES.TOP_METRICS, + expressionName: aggTopMetricsFnName, + title: i18n.translate('data.search.aggs.metrics.topMetricsTitle', { + defaultMessage: 'Top metrics', + }), + makeLabel(aggConfig) { + const isDescOrder = aggConfig.getParam('sortOrder').value === 'desc'; + const size = aggConfig.getParam('size'); + const field = aggConfig.getParam('field'); + const sortField = aggConfig.getParam('sortField'); + + if (isDescOrder) { + if (size > 1) { + return i18n.translate('data.search.aggs.metrics.topMetrics.descWithSizeLabel', { + defaultMessage: `Last {size} "{fieldName}" values by "{sortField}"`, + values: { + size, + fieldName: field?.displayName, + sortField: sortField?.displayName ?? '_score', + }, + }); + } else { + return i18n.translate('data.search.aggs.metrics.topMetrics.descNoSizeLabel', { + defaultMessage: `Last "{fieldName}" value by "{sortField}"`, + values: { + fieldName: field?.displayName, + sortField: sortField?.displayName ?? '_score', + }, + }); + } + } else { + if (size > 1) { + return i18n.translate('data.search.aggs.metrics.topMetrics.ascWithSizeLabel', { + defaultMessage: `First {size} "{fieldName}" values by "{sortField}"`, + values: { + size, + fieldName: field?.displayName, + sortField: sortField?.displayName ?? '_score', + }, + }); + } else { + return i18n.translate('data.search.aggs.metrics.topMetrics.ascNoSizeLabel', { + defaultMessage: `First "{fieldName}" value by "{sortField}"`, + values: { + fieldName: field?.displayName, + sortField: sortField?.displayName ?? '_score', + }, + }); + } + } + }, + params: [ + { + name: 'field', + type: 'field', + scriptable: false, + filterFieldTypes: [ + KBN_FIELD_TYPES.STRING, + KBN_FIELD_TYPES.IP, + KBN_FIELD_TYPES.BOOLEAN, + KBN_FIELD_TYPES.NUMBER, + KBN_FIELD_TYPES.DATE, + ], + write(agg, output) { + const field = agg.getParam('field'); + output.params.metrics = { field: field.name }; + }, + }, + { + name: 'size', + default: 1, + }, + { + name: 'sortField', + type: 'field', + scriptable: false, + filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.DATE], + default(agg: IMetricAggConfig) { + return agg.getIndexPattern().timeFieldName; + }, + write: _.noop, // prevent default write, it is handled below + }, + { + name: 'sortOrder', + type: 'optioned', + default: 'desc', + options: [ + { + text: i18n.translate('data.search.aggs.metrics.topMetrics.descendingLabel', { + defaultMessage: 'Descending', + }), + value: 'desc', + }, + { + text: i18n.translate('data.search.aggs.metrics.topMetrics.ascendingLabel', { + defaultMessage: 'Ascending', + }), + value: 'asc', + }, + ], + write(agg, output) { + const sortField = agg.params.sortField; + const sortOrder = agg.params.sortOrder; + + if (sortField && sortOrder) { + output.params.sort = { + [sortField.name]: sortOrder.value, + }; + } else { + output.params.sort = '_score'; + } + }, + }, + ], + // override is needed to support top_metrics as an orderAgg of terms agg + getValueBucketPath(agg) { + const field = agg.getParam('field').name; + return `${agg.id}[${field}]`; + }, + getValue(agg, aggregate: Record) { + const metricFieldName = agg.getParam('field').name; + const results = aggregate[agg.id]?.top.map((result) => result.metrics[metricFieldName]) ?? []; + + if (results.length === 0) return null; + if (results.length === 1) return results[0]; + return results; + }, + }); +}; diff --git a/src/plugins/data/common/search/aggs/metrics/top_metrics_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/top_metrics_fn.test.ts new file mode 100644 index 00000000000000..848fccda283faa --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/top_metrics_fn.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggTopMetrics } from './top_metrics_fn'; + +describe('agg_expression_functions', () => { + describe('aggTopMetrics', () => { + const fn = functionWrapper(aggTopMetrics()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({ + field: 'machine.os.keyword', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "field": "machine.os.keyword", + "json": undefined, + "size": undefined, + "sortField": undefined, + "sortOrder": undefined, + }, + "schema": undefined, + "type": "top_metrics", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + id: '1', + enabled: false, + schema: 'whatever', + field: 'machine.os.keyword', + sortOrder: 'asc', + size: 6, + sortField: 'bytes', + }); + + expect(actual.value).toMatchInlineSnapshot(` + Object { + "enabled": false, + "id": "1", + "params": Object { + "customLabel": undefined, + "field": "machine.os.keyword", + "json": undefined, + "size": 6, + "sortField": "bytes", + "sortOrder": "asc", + }, + "schema": "whatever", + "type": "top_metrics", + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'machine.os.keyword', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual('{ "foo": true }'); + }); + }); +}); diff --git a/src/plugins/data/common/search/aggs/metrics/top_metrics_fn.ts b/src/plugins/data/common/search/aggs/metrics/top_metrics_fn.ts new file mode 100644 index 00000000000000..6fe9ba97fe4483 --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/top_metrics_fn.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; + +export const aggTopMetricsFnName = 'aggTopMetrics'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggTopMetricsFnName, + Input, + AggArgs, + Output +>; + +export const aggTopMetrics = (): FunctionDefinition => ({ + name: aggTopMetricsFnName, + help: i18n.translate('data.search.aggs.function.metrics.topMetrics.help', { + defaultMessage: 'Generates a serialized aggregation configuration for Top metrics.', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.topMetrics.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.metrics.topMetrics.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + size: { + types: ['number'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.size.help', { + defaultMessage: 'Number of top values to retrieve', + }), + }, + sortOrder: { + types: ['string'], + options: ['desc', 'asc'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.sortOrder.help', { + defaultMessage: 'Order in which to return the results: asc or desc', + }), + }, + sortField: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.sortField.help', { + defaultMessage: 'Field to order results by', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.json.help', { + defaultMessage: 'Advanced JSON to include when the aggregation is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.TOP_METRICS, + params: { + ...rest, + }, + }, + }; + }, +}); diff --git a/src/plugins/data/common/search/aggs/param_types/field.ts b/src/plugins/data/common/search/aggs/param_types/field.ts index 940fdafd548753..b56787121f7248 100644 --- a/src/plugins/data/common/search/aggs/param_types/field.ts +++ b/src/plugins/data/common/search/aggs/param_types/field.ts @@ -43,6 +43,7 @@ export class FieldParamType extends BaseParamType { this.filterFieldTypes = config.filterFieldTypes || '*'; this.onlyAggregatable = config.onlyAggregatable !== false; + this.scriptable = config.scriptable !== false; this.filterField = config.filterField; if (!config.write) { diff --git a/src/plugins/data/common/search/aggs/types.ts b/src/plugins/data/common/search/aggs/types.ts index cf9a6123b14c8d..edc328bcb5099a 100644 --- a/src/plugins/data/common/search/aggs/types.ts +++ b/src/plugins/data/common/search/aggs/types.ts @@ -93,6 +93,8 @@ import { import { AggParamsSampler } from './buckets/sampler'; import { AggParamsDiversifiedSampler } from './buckets/diversified_sampler'; import { AggParamsSignificantText } from './buckets/significant_text'; +import { AggParamsTopMetrics } from './metrics/top_metrics'; +import { aggTopMetrics } from './metrics/top_metrics_fn'; export type { IAggConfig, AggConfigSerialized } from './agg_config'; export type { CreateAggConfigParams, IAggConfigs } from './agg_configs'; @@ -187,6 +189,7 @@ export interface AggParamsMapping { [METRIC_TYPES.PERCENTILES]: AggParamsPercentiles; [METRIC_TYPES.SERIAL_DIFF]: AggParamsSerialDiff; [METRIC_TYPES.TOP_HITS]: AggParamsTopHit; + [METRIC_TYPES.TOP_METRICS]: AggParamsTopMetrics; } /** @@ -229,4 +232,5 @@ export interface AggFunctionsMapping { aggStdDeviation: ReturnType; aggSum: ReturnType; aggTopHit: ReturnType; + aggTopMetrics: ReturnType; } diff --git a/src/plugins/data/public/search/aggs/aggs_service.test.ts b/src/plugins/data/public/search/aggs/aggs_service.test.ts index 101c2c909c7e1d..83328e196fa0a5 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.test.ts @@ -54,7 +54,7 @@ describe('AggsService - public', () => { service.setup(setupDeps); const start = service.start(startDeps); expect(start.types.getAll().buckets.length).toBe(16); - expect(start.types.getAll().metrics.length).toBe(23); + expect(start.types.getAll().metrics.length).toBe(24); }); test('registers custom agg types', () => { @@ -71,7 +71,7 @@ describe('AggsService - public', () => { const start = service.start(startDeps); expect(start.types.getAll().buckets.length).toBe(17); expect(start.types.getAll().buckets.some(({ name }) => name === 'foo')).toBe(true); - expect(start.types.getAll().metrics.length).toBe(24); + expect(start.types.getAll().metrics.length).toBe(25); expect(start.types.getAll().metrics.some(({ name }) => name === 'bar')).toBe(true); }); }); diff --git a/src/plugins/vis_default_editor/public/components/agg_params_map.ts b/src/plugins/vis_default_editor/public/components/agg_params_map.ts index a61df61f2316c6..283e1d7511b750 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params_map.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_map.ts @@ -67,6 +67,11 @@ const metrics = { sortField: controls.TopSortFieldParamEditor, sortOrder: controls.OrderParamEditor, }, + [METRIC_TYPES.TOP_METRICS]: { + field: controls.FieldParamEditor, + sortField: controls.TopSortFieldParamEditor, + sortOrder: controls.OrderParamEditor, + }, [METRIC_TYPES.PERCENTILES]: { percents: controls.PercentilesEditor, }, diff --git a/src/plugins/vis_default_editor/public/components/controls/metric_agg.tsx b/src/plugins/vis_default_editor/public/components/controls/metric_agg.tsx index 1f844b50424746..2888d399bc0148 100644 --- a/src/plugins/vis_default_editor/public/components/controls/metric_agg.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/metric_agg.tsx @@ -13,7 +13,14 @@ import { i18n } from '@kbn/i18n'; import { useAvailableOptions, useFallbackMetric, useValidation } from './utils'; import { AggParamEditorProps } from '../agg_param_props'; -const aggFilter = ['!top_hits', '!percentiles', '!percentile_ranks', '!median', '!std_dev']; +const aggFilter = [ + '!top_hits', + '!top_metrics', + '!percentiles', + '!percentile_ranks', + '!median', + '!std_dev', +]; const EMPTY_VALUE = 'EMPTY_VALUE'; const DEFAULT_OPTIONS = [{ text: '', value: EMPTY_VALUE, hidden: true }]; diff --git a/test/interpreter_functional/test_suites/run_pipeline/esaggs_topmetrics.ts b/test/interpreter_functional/test_suites/run_pipeline/esaggs_topmetrics.ts new file mode 100644 index 00000000000000..4f43709ba4a7e0 --- /dev/null +++ b/test/interpreter_functional/test_suites/run_pipeline/esaggs_topmetrics.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { ExpectExpression, expectExpressionProvider } from './helpers'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; + +export default function ({ + getService, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { + let expectExpression: ExpectExpression; + + describe('esaggs_topmetrics', () => { + before(() => { + expectExpression = expectExpressionProvider({ getService, updateBaselines }); + }); + + const timeRange = { + from: '2015-09-21T00:00:00Z', + to: '2015-09-22T00:00:00Z', + }; + + describe('aggTopMetrics', () => { + it('can execute aggTopMetrics', async () => { + const expression = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggTerms id="1" enabled=true schema="bucket" field="extension.raw"} + aggs={aggTopMetrics id="2" enabled=true schema="metric" field="bytes" sortField="@timestamp" sortOrder="desc" size=3 } + `; + const result = await expectExpression('aggTopMetrics', expression).getResponse(); + + expect(result.rows.map((r: { 'col-0-1': string }) => r['col-0-1'])).to.eql([ + 'jpg', + 'css', + 'png', + 'gif', + 'php', + ]); + + result.rows.forEach((r: { 'col-1-2': number[] }) => { + expect(r['col-1-2'].length).to.be(3); + expect( + r['col-1-2'].forEach((metric) => { + expect(typeof metric).to.be('number'); + }) + ); + }); + }); + + it('can execute aggTopMetrics with different sortOrder and size', async () => { + const expression = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggTerms id="1" enabled=true schema="bucket" field="extension.raw"} + aggs={aggTopMetrics id="2" enabled=true schema="metric" field="bytes" sortField="@timestamp" sortOrder="asc" size=1 } + `; + const result = await expectExpression('aggTopMetrics', expression).getResponse(); + + expect(result.rows.map((r: { 'col-0-1': string }) => r['col-0-1'])).to.eql([ + 'jpg', + 'css', + 'png', + 'gif', + 'php', + ]); + + result.rows.forEach((r: { 'col-1-2': number[] }) => { + expect(typeof r['col-1-2']).to.be('number'); + }); + }); + + it('can use aggTopMetrics as an orderAgg of aggTerms', async () => { + const expressionSortBytesAsc = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggTerms id="1" enabled=true schema="bucket" field="extension.raw" size=1 orderAgg={aggTopMetrics id="order" enabled=true schema="metric" field="bytes" sortField="@timestamp" sortOrder="asc" size=1}} + aggs={aggCount id="2" enabled=true schema="metric"} + `; + + const resultSortBytesAsc = await expectExpression( + 'sortBytesAsc', + expressionSortBytesAsc + ).getResponse(); + + const expressionSortBytesDesc = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggTerms id="1" enabled=true schema="bucket" field="extension.raw" size=1 orderAgg={aggTopMetrics id="order" enabled=true schema="metric" field="bytes" sortField="@timestamp" sortOrder="desc" size=1}} + aggs={aggCount id="2" enabled=true schema="metric"} + `; + + const resultSortBytesDesc = await expectExpression( + 'sortBytesDesc', + expressionSortBytesDesc + ).getResponse(); + + expect(resultSortBytesAsc.rows.length).to.be(1); + expect(resultSortBytesAsc.rows[0]['col-0-1']).to.be('jpg'); + + expect(resultSortBytesDesc.rows.length).to.be(1); + expect(resultSortBytesDesc.rows[0]['col-0-1']).to.be('php'); + }); + }); + }); +} diff --git a/test/interpreter_functional/test_suites/run_pipeline/index.ts b/test/interpreter_functional/test_suites/run_pipeline/index.ts index 97387fc0a965fc..e24563a5918eb8 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/index.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/index.ts @@ -25,6 +25,7 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'Australia/North', defaultIndex: 'logstash-*', + 'bfetch:disableCompression': true, // makes it easier to debug while developing tests }); await browser.setWindowSize(1300, 900); await PageObjects.common.navigateToApp('settings'); @@ -47,5 +48,6 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid loadTestFile(require.resolve('./esaggs_sampler')); loadTestFile(require.resolve('./esaggs_significanttext')); loadTestFile(require.resolve('./esaggs_rareterms')); + loadTestFile(require.resolve('./esaggs_topmetrics')); }); } From 73f7ac419f75cfae5ab840a072aea95a4f0f72d0 Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Mon, 7 Mar 2022 17:02:16 +0100 Subject: [PATCH 13/19] Fix Pagination is not working correctly for host by risk under hosts tab (#126609) * Fix Pagination is not working correctly for host by risk under hosts tab * Fix changing items per page doesn't work * Fix next page button enabled when the table is empty --- .../integration/hosts/host_risk_tab.spec.ts | 31 ++++- .../overview/risky_hosts_panel.spec.ts | 2 +- .../cypress/screens/hosts/host_risk.ts | 11 ++ .../cypress/tasks/host_risk.ts | 22 ++- .../components/paginated_table/index.test.tsx | 27 ++++ .../components/paginated_table/index.tsx | 14 +- .../containers/host_risk_score/index.tsx | 2 +- .../es_archives/risky_hosts/data.json | 129 +++++++++++++++++- 8 files changed, 222 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/hosts/host_risk_tab.spec.ts b/x-pack/plugins/security_solution/cypress/integration/hosts/host_risk_tab.spec.ts index 38a639e19c6b87..3af77036649aa1 100644 --- a/x-pack/plugins/security_solution/cypress/integration/hosts/host_risk_tab.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/hosts/host_risk_tab.spec.ts @@ -7,14 +7,20 @@ import { cleanKibana } from '../../tasks/common'; import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; -import { navigateToHostRiskDetailTab } from '../../tasks/host_risk'; +import { + navigateToHostRiskDetailTab, + openRiskTableFilterAndSelectTheCriticalOption, + removeCritialFilter, + selectFiveItemsPerPageOption, +} from '../../tasks/host_risk'; import { HOST_BY_RISK_TABLE_CELL, - HOST_BY_RISK_TABLE_FILTER, - HOST_BY_RISK_TABLE_FILTER_CRITICAL, + HOST_BY_RISK_TABLE_HOSTNAME_CELL, + HOST_BY_RISK_TABLE_NEXT_PAGE_BUTTON, } from '../../screens/hosts/host_risk'; import { loginAndWaitForPage } from '../../tasks/login'; import { HOSTS_URL } from '../../urls/navigation'; +import { clearSearchBar, kqlSearch } from '../../tasks/security_header'; describe('risk tab', () => { before(() => { @@ -29,15 +35,30 @@ describe('risk tab', () => { }); it('renders the table', () => { + kqlSearch('host.name: "siem-kibana" {enter}'); cy.get(HOST_BY_RISK_TABLE_CELL).eq(3).should('have.text', 'siem-kibana'); cy.get(HOST_BY_RISK_TABLE_CELL).eq(4).should('have.text', '21.00'); cy.get(HOST_BY_RISK_TABLE_CELL).eq(5).should('have.text', 'Low'); + clearSearchBar(); }); it('filters the table', () => { - cy.get(HOST_BY_RISK_TABLE_FILTER).click(); - cy.get(HOST_BY_RISK_TABLE_FILTER_CRITICAL).click(); + openRiskTableFilterAndSelectTheCriticalOption(); cy.get(HOST_BY_RISK_TABLE_CELL).eq(3).should('not.have.text', 'siem-kibana'); + + removeCritialFilter(); + }); + + it('should be able to change items count per page', () => { + selectFiveItemsPerPageOption(); + + cy.get(HOST_BY_RISK_TABLE_HOSTNAME_CELL).should('have.length', 5); + }); + + it('should not allow page change when page is empty', () => { + kqlSearch('host.name: "nonexistent_host" {enter}'); + cy.get(HOST_BY_RISK_TABLE_NEXT_PAGE_BUTTON).should(`not.exist`); + clearSearchBar(); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/overview/risky_hosts_panel.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview/risky_hosts_panel.spec.ts index 1c55a38b324953..652b3c1118b30e 100644 --- a/x-pack/plugins/security_solution/cypress/integration/overview/risky_hosts_panel.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/overview/risky_hosts_panel.spec.ts @@ -69,7 +69,7 @@ describe('Risky Hosts Link Panel', () => { `${OVERVIEW_RISKY_HOSTS_LINKS} ${OVERVIEW_RISKY_HOSTS_LINKS_WARNING_INNER_PANEL}` ).should('not.exist'); cy.get(`${OVERVIEW_RISKY_HOSTS_VIEW_DASHBOARD_BUTTON}`).should('be.disabled'); - cy.get(`${OVERVIEW_RISKY_HOSTS_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 1 host'); + cy.get(`${OVERVIEW_RISKY_HOSTS_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 6 hosts'); changeSpace(testSpaceName); cy.visit(`/s/${testSpaceName}${OVERVIEW_URL}`); diff --git a/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts index 58331518255df7..3209200cf25a18 100644 --- a/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts +++ b/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts @@ -24,3 +24,14 @@ export const HOST_BY_RISK_TABLE_FILTER = '[data-test-subj="host-risk-filter-butt export const HOST_BY_RISK_TABLE_FILTER_CRITICAL = '[data-test-subj="host-risk-filter-item-Critical"]'; + +export const HOST_BY_RISK_TABLE_PERPAGE_BUTTON = + '[data-test-subj="loadingMoreSizeRowPopover"] button'; + +export const HOST_BY_RISK_TABLE_PERPAGE_OPTIONS = + '[data-test-subj="loadingMorePickSizeRow"] button'; + +export const HOST_BY_RISK_TABLE_NEXT_PAGE_BUTTON = + '[data-test-subj="numberedPagination"] [data-test-subj="pagination-button-next"]'; + +export const HOST_BY_RISK_TABLE_HOSTNAME_CELL = '[data-test-subj="render-content-host.name"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/host_risk.ts b/x-pack/plugins/security_solution/cypress/tasks/host_risk.ts index 7a357e8a5c7fb2..afa04bb6de0ca9 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/host_risk.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/host_risk.ts @@ -5,7 +5,15 @@ * 2.0. */ -import { LOADING_TABLE, RISK_DETAILS_NAV, RISK_FLYOUT_TRIGGER } from '../screens/hosts/host_risk'; +import { + HOST_BY_RISK_TABLE_FILTER, + HOST_BY_RISK_TABLE_FILTER_CRITICAL, + HOST_BY_RISK_TABLE_PERPAGE_BUTTON, + HOST_BY_RISK_TABLE_PERPAGE_OPTIONS, + LOADING_TABLE, + RISK_DETAILS_NAV, + RISK_FLYOUT_TRIGGER, +} from '../screens/hosts/host_risk'; export const navigateToHostRiskDetailTab = () => cy.get(RISK_DETAILS_NAV).click(); @@ -15,3 +23,15 @@ export const waitForTableToLoad = () => { cy.get(LOADING_TABLE).should('exist'); cy.get(LOADING_TABLE).should('not.exist'); }; + +export const openRiskTableFilterAndSelectTheCriticalOption = () => { + cy.get(HOST_BY_RISK_TABLE_FILTER).click(); + cy.get(HOST_BY_RISK_TABLE_FILTER_CRITICAL).click(); +}; +export const removeCritialFilter = () => { + cy.get(HOST_BY_RISK_TABLE_FILTER_CRITICAL).click(); +}; +export const selectFiveItemsPerPageOption = () => { + cy.get(HOST_BY_RISK_TABLE_PERPAGE_BUTTON).click(); + cy.get(HOST_BY_RISK_TABLE_PERPAGE_OPTIONS).first().click(); +}; diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx index 64c3584bc668cd..0c09dce9c07cb3 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx @@ -327,6 +327,33 @@ describe('Paginated Table Component', () => { ); expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeFalsy(); }); + + test('Should hide pagination if totalCount is zero', () => { + const wrapper = mount( + + {'My test supplement.'}

} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={DEFAULT_MAX_TABLE_QUERY_SIZE} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={0} + updateActivePage={updateActivePage} + updateLimitPagination={(limit) => updateLimitPagination({ limit })} + /> +
+ ); + + expect(wrapper.find('[data-test-subj="numberedPagination"]').exists()).toBeFalsy(); + }); }); describe('Events', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx index 6100c03d38bfa1..310ab039057c2f 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx @@ -303,12 +303,14 @@ const PaginatedTableComponent: FC = ({ - + {totalCount > 0 && ( + + )} {(isInspect || myLoading) && ( diff --git a/x-pack/plugins/security_solution/public/hosts/containers/host_risk_score/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/host_risk_score/index.tsx index 7f2c41f1414cfc..516895d49b8667 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/host_risk_score/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/host_risk_score/index.tsx @@ -180,7 +180,7 @@ export const useHostRiskScore = ({ factoryQueryType: HostsQueries.hostsRiskScore, filterQuery: createFilter(filterQuery), pagination: - cursorStart && querySize + cursorStart !== undefined && querySize !== undefined ? { cursorStart, querySize, diff --git a/x-pack/test/security_solution_cypress/es_archives/risky_hosts/data.json b/x-pack/test/security_solution_cypress/es_archives/risky_hosts/data.json index cde819a836b0a3..3e468d7a84ca26 100644 --- a/x-pack/test/security_solution_cypress/es_archives/risky_hosts/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/risky_hosts/data.json @@ -13,7 +13,7 @@ "rule_risk": 42 } ] - }, + }, "host":{ "name":"siem-kibana" }, @@ -23,6 +23,131 @@ } } +{ + "type":"doc", + "value":{ + "id":"a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb71f", + "index":"ml_host_risk_score_latest_default", + "source":{ + "@timestamp":"2021-03-10T14:51:05.766Z", + "risk_stats": { + "risk_score": 50, + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + }, + "host":{ + "name":"fake-1" + }, + "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", + "risk":"Moderate" + } + } +} + +{ + "type":"doc", + "value":{ + "id":"a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb72f", + "index":"ml_host_risk_score_latest_default", + "source":{ + "@timestamp":"2021-03-10T14:51:05.766Z", + "risk_stats": { + "risk_score": 50, + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + }, + "host":{ + "name":"fake-2" + }, + "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", + "risk":"Moderate" + } + } +} + +{ + "type":"doc", + "value":{ + "id":"a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb73f", + "index":"ml_host_risk_score_latest_default", + "source":{ + "@timestamp":"2021-03-10T14:51:05.766Z", + "risk_stats": { + "risk_score": 50, + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + }, + "host":{ + "name":"fake-3" + }, + "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", + "risk":"Moderate" + } + } +} + +{ + "type":"doc", + "value":{ + "id":"a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f", + "index":"ml_host_risk_score_latest_default", + "source":{ + "@timestamp":"2021-03-10T14:51:05.766Z", + "risk_stats": { + "risk_score": 50, + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + }, + "host":{ + "name":"fake-4" + }, + "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", + "risk":"Moderate" + } + } +} + +{ + "type":"doc", + "value":{ + "id":"a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb75f", + "index":"ml_host_risk_score_latest_default", + "source":{ + "@timestamp":"2021-03-10T14:51:05.766Z", + "risk_stats": { + "risk_score": 50, + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + }, + "host":{ + "name":"fake-5" + }, + "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", + "risk":"Moderate" + } + } +} + { "type":"doc", "value":{ @@ -38,7 +163,7 @@ "rule_risk": 42 } ] - }, + }, "host":{ "name":"siem-kibana" }, From 004f0d4daa042afa2f1b9fbd74a44b9a4b4755b0 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 7 Mar 2022 08:24:04 -0800 Subject: [PATCH 14/19] [type-summarizer/integration] fix flaky tests (#127038) --- .../src/lib/source_mapper.ts | 20 +++++++++++-------- .../tests/integration_tests/class.test.ts | 2 +- .../tests/integration_tests/function.test.ts | 6 ++++-- .../integration_tests/import_boundary.test.ts | 4 ++-- .../tests/integration_tests/interface.test.ts | 2 +- .../integration_tests/references.test.ts | 8 +++++--- .../integration_tests/type_alias.test.ts | 2 +- .../tests/integration_tests/variables.test.ts | 2 +- 8 files changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/kbn-type-summarizer/src/lib/source_mapper.ts b/packages/kbn-type-summarizer/src/lib/source_mapper.ts index f6075684e04a6d..0b0e69571469c3 100644 --- a/packages/kbn-type-summarizer/src/lib/source_mapper.ts +++ b/packages/kbn-type-summarizer/src/lib/source_mapper.ts @@ -17,6 +17,8 @@ import { tryReadFile } from './helpers/fs'; import { parseJson } from './helpers/json'; import { isNodeModule } from './is_node_module'; +type SourceMapConsumerEntry = [ts.SourceFile, BasicSourceMapConsumer | undefined]; + export class SourceMapper { static async forSourceFiles( log: Logger, @@ -24,10 +26,8 @@ export class SourceMapper { repoRelativePackageDir: string, sourceFiles: readonly ts.SourceFile[] ) { - const consumers = new Map(); - - await Promise.all( - sourceFiles.map(async (sourceFile) => { + const entries = await Promise.all( + sourceFiles.map(async (sourceFile): Promise => { if (isNodeModule(dtsDir, sourceFile.fileName)) { return; } @@ -35,8 +35,7 @@ export class SourceMapper { const text = sourceFile.getText(); const match = text.match(/^\/\/#\s*sourceMappingURL=(.*)/im); if (!match) { - consumers.set(sourceFile, undefined); - return; + return [sourceFile, undefined]; } const relSourceFile = Path.relative(process.cwd(), sourceFile.fileName); @@ -50,11 +49,16 @@ export class SourceMapper { } const json = parseJson(sourceJson, `source map at [${relSourceMapPath}]`); - consumers.set(sourceFile, await new SourceMapConsumer(json)); - log.debug('loaded sourcemap for', relSourceFile); + return [sourceFile, await new SourceMapConsumer(json)]; }) ); + const consumers = new Map(entries.filter((e): e is SourceMapConsumerEntry => !!e)); + log.debug( + 'loaded sourcemaps for', + Array.from(consumers.keys()).map((s) => Path.relative(process.cwd(), s.fileName)) + ); + return new SourceMapper(consumers, repoRelativePackageDir); } diff --git a/packages/kbn-type-summarizer/tests/integration_tests/class.test.ts b/packages/kbn-type-summarizer/tests/integration_tests/class.test.ts index 84c1ee80c5f166..d2b39ab69d47bf 100644 --- a/packages/kbn-type-summarizer/tests/integration_tests/class.test.ts +++ b/packages/kbn-type-summarizer/tests/integration_tests/class.test.ts @@ -69,7 +69,7 @@ it('prints basic class correctly', async () => { } `); expect(output.logs).toMatchInlineSnapshot(` - "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + "debug loaded sourcemaps for [ 'packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts' ] debug Ignoring 1 global declarations for \\"Record\\" debug Ignoring 5 global declarations for \\"Promise\\" " diff --git a/packages/kbn-type-summarizer/tests/integration_tests/function.test.ts b/packages/kbn-type-summarizer/tests/integration_tests/function.test.ts index 6afc04afe8faad..ec15d941ca1536 100644 --- a/packages/kbn-type-summarizer/tests/integration_tests/function.test.ts +++ b/packages/kbn-type-summarizer/tests/integration_tests/function.test.ts @@ -74,8 +74,10 @@ it('prints the function declaration, including comments', async () => { } `); expect(result.logs).toMatchInlineSnapshot(` - "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/bar.d.ts - debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + "debug loaded sourcemaps for [ + 'packages/kbn-type-summarizer/tests/__tmp__/dist_dts/bar.d.ts', + 'packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts' + ] " `); }); diff --git a/packages/kbn-type-summarizer/tests/integration_tests/import_boundary.test.ts b/packages/kbn-type-summarizer/tests/integration_tests/import_boundary.test.ts index f23b6c3656d508..35cf08e2973598 100644 --- a/packages/kbn-type-summarizer/tests/integration_tests/import_boundary.test.ts +++ b/packages/kbn-type-summarizer/tests/integration_tests/import_boundary.test.ts @@ -52,7 +52,7 @@ it('output type links to named import from node modules', async () => { } `); expect(output.logs).toMatchInlineSnapshot(` - "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + "debug loaded sourcemaps for [ 'packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts' ] " `); }); @@ -84,7 +84,7 @@ it('output type links to default import from node modules', async () => { } `); expect(output.logs).toMatchInlineSnapshot(` - "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + "debug loaded sourcemaps for [ 'packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts' ] " `); }); diff --git a/packages/kbn-type-summarizer/tests/integration_tests/interface.test.ts b/packages/kbn-type-summarizer/tests/integration_tests/interface.test.ts index da53e91302eef0..cc821f1c9fc90f 100644 --- a/packages/kbn-type-summarizer/tests/integration_tests/interface.test.ts +++ b/packages/kbn-type-summarizer/tests/integration_tests/interface.test.ts @@ -55,7 +55,7 @@ it('prints the whole interface, including comments', async () => { } `); expect(result.logs).toMatchInlineSnapshot(` - "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + "debug loaded sourcemaps for [ 'packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts' ] debug Ignoring 5 global declarations for \\"Promise\\" " `); diff --git a/packages/kbn-type-summarizer/tests/integration_tests/references.test.ts b/packages/kbn-type-summarizer/tests/integration_tests/references.test.ts index 1733b43694000d..796bcd5fac3d1d 100644 --- a/packages/kbn-type-summarizer/tests/integration_tests/references.test.ts +++ b/packages/kbn-type-summarizer/tests/integration_tests/references.test.ts @@ -59,9 +59,11 @@ it('collects references from source files which contribute to result', async () } `); expect(result.logs).toMatchInlineSnapshot(` - "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/files/foo.d.ts - debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/files/index.d.ts - debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + "debug loaded sourcemaps for [ + 'packages/kbn-type-summarizer/tests/__tmp__/dist_dts/files/foo.d.ts', + 'packages/kbn-type-summarizer/tests/__tmp__/dist_dts/files/index.d.ts', + 'packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts' + ] debug Ignoring 5 global declarations for \\"Promise\\" debug Ignoring 4 global declarations for \\"Symbol\\" debug Ignoring 2 global declarations for \\"Component\\" diff --git a/packages/kbn-type-summarizer/tests/integration_tests/type_alias.test.ts b/packages/kbn-type-summarizer/tests/integration_tests/type_alias.test.ts index 79c2ea69b94777..f099bad9f3de6a 100644 --- a/packages/kbn-type-summarizer/tests/integration_tests/type_alias.test.ts +++ b/packages/kbn-type-summarizer/tests/integration_tests/type_alias.test.ts @@ -36,7 +36,7 @@ it('prints basic type alias', async () => { } `); expect(output.logs).toMatchInlineSnapshot(` - "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + "debug loaded sourcemaps for [ 'packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts' ] " `); }); diff --git a/packages/kbn-type-summarizer/tests/integration_tests/variables.test.ts b/packages/kbn-type-summarizer/tests/integration_tests/variables.test.ts index daa6abcc34c594..c51c9b0098b6c6 100644 --- a/packages/kbn-type-summarizer/tests/integration_tests/variables.test.ts +++ b/packages/kbn-type-summarizer/tests/integration_tests/variables.test.ts @@ -62,7 +62,7 @@ it('prints basic variable exports with sourcemaps', async () => { } `); expect(output.logs).toMatchInlineSnapshot(` - "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + "debug loaded sourcemaps for [ 'packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts' ] " `); }); From a4febd770916997b6355653089e7d7210bde0d67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Mon, 7 Mar 2022 11:38:58 -0500 Subject: [PATCH 15/19] Allow access to ElasticsearchClient's child function from core's TS interface (#126731) * Expose child from core ElasticsearchClient * Update docs * Fix typecheck Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...-plugin-core-server.elasticsearchclient.md | 2 +- src/core/server/elasticsearch/client/types.ts | 2 +- src/core/server/server.api.md | 2 +- .../lib/wrap_scoped_cluster_client.test.ts | 25 +++++-------------- .../server/lib/wrap_scoped_cluster_client.ts | 5 ++-- x-pack/plugins/alerting/server/types.ts | 6 ----- 6 files changed, 11 insertions(+), 31 deletions(-) diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchclient.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchclient.md index 9a04a1d5817658..1dfb1ab7a0b424 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchclient.md @@ -9,5 +9,5 @@ Client used to query the elasticsearch cluster. Signature: ```typescript -export declare type ElasticsearchClient = Omit; +export declare type ElasticsearchClient = Omit; ``` diff --git a/src/core/server/elasticsearch/client/types.ts b/src/core/server/elasticsearch/client/types.ts index 68fbc87193074f..17248e491962d5 100644 --- a/src/core/server/elasticsearch/client/types.ts +++ b/src/core/server/elasticsearch/client/types.ts @@ -15,7 +15,7 @@ import type { Client } from '@elastic/elasticsearch'; */ export type ElasticsearchClient = Omit< Client, - 'connectionPool' | 'serializer' | 'extend' | 'child' | 'close' | 'diagnostic' + 'connectionPool' | 'serializer' | 'extend' | 'close' | 'diagnostic' >; /** diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 5fe1942ed8453b..6d5b06346225b7 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -886,7 +886,7 @@ export { EcsEventOutcome } export { EcsEventType } // @public -export type ElasticsearchClient = Omit; +export type ElasticsearchClient = Omit; // @public export type ElasticsearchClientConfig = Pick & { diff --git a/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.test.ts b/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.test.ts index 15f5a37edb910e..d6101c9c6cec4f 100644 --- a/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.test.ts +++ b/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.test.ts @@ -9,7 +9,6 @@ import { Client } from '@elastic/elasticsearch'; import { loggingSystemMock } from 'src/core/server/mocks'; import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; import { createWrappedScopedClusterClientFactory } from './wrap_scoped_cluster_client'; -import { ElasticsearchClientWithChild } from '../types'; const esQuery = { body: { query: { bool: { filter: { range: { '@timestamp': { gte: 0 } } } } } }, @@ -41,9 +40,7 @@ describe('wrapScopedClusterClient', () => { const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); const childClient = elasticsearchServiceMock.createElasticsearchClient(); - ( - scopedClusterClient.asInternalUser as unknown as jest.Mocked - ).child.mockReturnValue(childClient as unknown as Client); + scopedClusterClient.asInternalUser.child.mockReturnValue(childClient as unknown as Client); const asInternalUserWrappedSearchFn = childClient.search; const wrappedSearchClient = createWrappedScopedClusterClientFactory({ @@ -62,9 +59,7 @@ describe('wrapScopedClusterClient', () => { const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); const childClient = elasticsearchServiceMock.createElasticsearchClient(); - ( - scopedClusterClient.asCurrentUser as unknown as jest.Mocked - ).child.mockReturnValue(childClient as unknown as Client); + scopedClusterClient.asCurrentUser.child.mockReturnValue(childClient as unknown as Client); const asCurrentUserWrappedSearchFn = childClient.search; const wrappedSearchClient = createWrappedScopedClusterClientFactory({ @@ -83,9 +78,7 @@ describe('wrapScopedClusterClient', () => { const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); const childClient = elasticsearchServiceMock.createElasticsearchClient(); - ( - scopedClusterClient.asInternalUser as unknown as jest.Mocked - ).child.mockReturnValue(childClient as unknown as Client); + scopedClusterClient.asInternalUser.child.mockReturnValue(childClient as unknown as Client); const asInternalUserWrappedSearchFn = childClient.search; const wrappedSearchClient = createWrappedScopedClusterClientFactory({ @@ -106,9 +99,7 @@ describe('wrapScopedClusterClient', () => { const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); const childClient = elasticsearchServiceMock.createElasticsearchClient(); - ( - scopedClusterClient.asInternalUser as unknown as jest.Mocked - ).child.mockReturnValue(childClient as unknown as Client); + scopedClusterClient.asInternalUser.child.mockReturnValue(childClient as unknown as Client); const asInternalUserWrappedSearchFn = childClient.search; asInternalUserWrappedSearchFn.mockRejectedValueOnce(new Error('something went wrong!')); @@ -127,9 +118,7 @@ describe('wrapScopedClusterClient', () => { const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); const childClient = elasticsearchServiceMock.createElasticsearchClient(); - ( - scopedClusterClient.asInternalUser as unknown as jest.Mocked - ).child.mockReturnValue(childClient as unknown as Client); + scopedClusterClient.asInternalUser.child.mockReturnValue(childClient as unknown as Client); const asInternalUserWrappedSearchFn = childClient.search; // @ts-ignore incomplete return type asInternalUserWrappedSearchFn.mockResolvedValue({}); @@ -156,9 +145,7 @@ describe('wrapScopedClusterClient', () => { const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); const childClient = elasticsearchServiceMock.createElasticsearchClient(); - ( - scopedClusterClient.asInternalUser as unknown as jest.Mocked - ).child.mockReturnValue(childClient as unknown as Client); + scopedClusterClient.asInternalUser.child.mockReturnValue(childClient as unknown as Client); const asInternalUserWrappedSearchFn = childClient.search; // @ts-ignore incomplete return type asInternalUserWrappedSearchFn.mockResolvedValue({ took: 333 }); diff --git a/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.ts b/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.ts index dfe32a48ce4384..2b71f95cd9f1c6 100644 --- a/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.ts +++ b/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.ts @@ -21,7 +21,7 @@ import type { AggregationsAggregate, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { IScopedClusterClient, ElasticsearchClient, Logger } from 'src/core/server'; -import { ElasticsearchClientWithChild, RuleExecutionMetrics } from '../types'; +import { RuleExecutionMetrics } from '../types'; import { Alert as Rule } from '../types'; type RuleInfo = Pick & { spaceId: string }; @@ -87,8 +87,7 @@ function wrapScopedClusterClient(opts: WrapScopedClusterClientOpts): IScopedClus function wrapEsClient(opts: WrapEsClientOpts): ElasticsearchClient { const { esClient, ...rest } = opts; - // Core hides access to .child via TS - const wrappedClient = (esClient as ElasticsearchClientWithChild).child({}); + const wrappedClient = esClient.child({}); // Mutating the functions we want to wrap wrappedClient.search = getWrappedSearchFn({ esClient: wrappedClient, ...rest }); diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 6b06f7efe30660..5499ba0c76caf6 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -5,12 +5,10 @@ * 2.0. */ -import { Client } from '@elastic/elasticsearch'; import type { IRouter, RequestHandlerContext, SavedObjectReference, - ElasticsearchClient, IUiSettingsClient, } from 'src/core/server'; import type { PublicMethodsOf } from '@kbn/utility-types'; @@ -48,10 +46,6 @@ import { IAbortableClusterClient } from './lib/create_abortable_es_client_factor export type WithoutQueryAndParams = Pick>; export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined; -export interface ElasticsearchClientWithChild extends ElasticsearchClient { - child: Client['child']; -} - /** * @public */ From 23ac12ba7b3da78356a32127b453a68708e86f50 Mon Sep 17 00:00:00 2001 From: Sander Philipse <94373878+sphilipse@users.noreply.github.com> Date: Mon, 7 Mar 2022 17:39:36 +0100 Subject: [PATCH 16/19] [Workplace Search] Connect button no longer gets stuck on connection error (#126771) --- .../components/add_source/add_source_logic.test.ts | 2 ++ .../components/add_source/connect_instance.test.tsx | 3 +-- .../components/add_source/connect_instance.tsx | 13 ++++--------- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts index 80f8a2fc18218d..a633beac3a1c23 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts @@ -664,11 +664,13 @@ describe('AddSourceLogic', () => { }); it('handles error', async () => { + const setButtonNotLoadingSpy = jest.spyOn(AddSourceLogic.actions, 'setButtonNotLoading'); http.post.mockReturnValue(Promise.reject('this is an error')); AddSourceLogic.actions.createContentSource(serviceType, successCallback, errorCallback); await nextTick(); + expect(setButtonNotLoadingSpy).toHaveBeenCalled(); expect(errorCallback).toHaveBeenCalled(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx index 0ee80019ea720e..0ae176dbef019f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx @@ -36,9 +36,8 @@ describe('ConnectInstance', () => { const getSourceConnectData = jest.fn((_, redirectOauth) => { redirectOauth(); }); - const createContentSource = jest.fn((_, redirectFormCreated, handleFormSubmitError) => { + const createContentSource = jest.fn((_, redirectFormCreated) => { redirectFormCreated(); - handleFormSubmitError(); }); const credentialsSourceData = staticSourceData[13]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx index a9e24c7b944aba..352addd8176d84 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState, useEffect, FormEvent } from 'react'; +import React, { useEffect, FormEvent } from 'react'; import { useActions, useValues } from 'kea'; @@ -51,8 +51,6 @@ export const ConnectInstance: React.FC = ({ onFormCreated, header, }) => { - const [formLoading, setFormLoading] = useState(false); - const { hasPlatinumLicense } = useValues(LicensingLogic); const { @@ -64,7 +62,7 @@ export const ConnectInstance: React.FC = ({ setSourceIndexPermissionsValue, } = useActions(AddSourceLogic); - const { loginValue, passwordValue, indexPermissionsValue, subdomainValue } = + const { buttonLoading, loginValue, passwordValue, indexPermissionsValue, subdomainValue } = useValues(AddSourceLogic); const { isOrganization } = useValues(AppLogic); @@ -77,12 +75,9 @@ export const ConnectInstance: React.FC = ({ const redirectOauth = (oauthUrl: string) => window.location.replace(oauthUrl); const redirectFormCreated = () => onFormCreated(name); const onOauthFormSubmit = () => getSourceConnectData(serviceType, redirectOauth); - const handleFormSubmitError = () => setFormLoading(false); - const onCredentialsFormSubmit = () => - createContentSource(serviceType, redirectFormCreated, handleFormSubmitError); + const onCredentialsFormSubmit = () => createContentSource(serviceType, redirectFormCreated); const handleFormSubmit = (e: FormEvent) => { - setFormLoading(true); e.preventDefault(); const onSubmit = hasOauthRedirect ? onOauthFormSubmit : onCredentialsFormSubmit; onSubmit(); @@ -145,7 +140,7 @@ export const ConnectInstance: React.FC = ({ {permissionsExcluded && !hasPlatinumLicense && } - + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.contentSource.connect.button', { defaultMessage: 'Connect {name}', values: { name }, From f47f89b58cb67fdf94fccce8bb6976697069ccaa Mon Sep 17 00:00:00 2001 From: Sander Philipse <94373878+sphilipse@users.noreply.github.com> Date: Mon, 7 Mar 2022 17:40:15 +0100 Subject: [PATCH 17/19] [Workplace Search] Make SharePoint Online users start with the Oauth intro instead of the choice page (#126908) --- .../components/add_source/add_source.test.tsx | 32 +++++++++++++++++++ .../components/add_source/add_source.tsx | 13 +++++++- .../components/add_source/add_source_logic.ts | 1 + .../add_source/configuration_choice.test.tsx | 30 ++++++----------- .../add_source/configuration_choice.tsx | 31 ++++++++---------- .../views/content_sources/sources_router.tsx | 3 +- 6 files changed, 71 insertions(+), 39 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx index 4598ca337f4e2c..76c6c3cfa9d592 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx @@ -27,6 +27,7 @@ import { staticSourceData } from '../../source_data'; import { AddSource } from './add_source'; import { AddSourceSteps } from './add_source_logic'; import { ConfigCompleted } from './config_completed'; +import { ConfigurationChoice } from './configuration_choice'; import { ConfigurationIntro } from './configuration_intro'; import { ConfigureOauth } from './configure_oauth'; import { ConnectInstance } from './connect_instance'; @@ -71,6 +72,22 @@ describe('AddSourceList', () => { expect(setAddSourceStep).toHaveBeenCalledWith(AddSourceSteps.SaveConfigStep); }); + it('renders default state correctly when there are multiple connector options', () => { + const wrapper = shallow( + + ); + wrapper.find(ConfigurationIntro).prop('advanceStep')(); + + expect(setAddSourceStep).toHaveBeenCalledWith(AddSourceSteps.ChoiceStep); + }); + describe('layout', () => { it('renders the default workplace search layout when on an organization view', () => { setMockValues({ ...mockValues, isOrganization: true }); @@ -153,4 +170,19 @@ describe('AddSourceList', () => { expect(wrapper.find(Reauthenticate)).toHaveLength(1); }); + + it('renders Config Choice step', () => { + setMockValues({ + ...mockValues, + addSourceCurrentStep: AddSourceSteps.ChoiceStep, + }); + const wrapper = shallow(); + const advance = wrapper.find(ConfigurationChoice).prop('goToInternalStep'); + expect(advance).toBeDefined(); + if (advance) { + advance(); + } + + expect(setAddSourceStep).toHaveBeenCalledWith(AddSourceSteps.SaveConfigStep); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx index 1e9be74224c5ed..f03c77290f22dd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx @@ -21,9 +21,12 @@ import { import { NAV } from '../../../../constants'; import { SOURCES_PATH, getSourcesPath, getAddPath } from '../../../../routes'; +import { hasMultipleConnectorOptions } from '../../../../utils'; + import { AddSourceHeader } from './add_source_header'; import { AddSourceLogic, AddSourceProps, AddSourceSteps } from './add_source_logic'; import { ConfigCompleted } from './config_completed'; +import { ConfigurationChoice } from './configuration_choice'; import { ConfigurationIntro } from './configuration_intro'; import { ConfigureOauth } from './configure_oauth'; import { ConnectInstance } from './connect_instance'; @@ -51,6 +54,7 @@ export const AddSource: React.FC = (props) => { const goToSaveConfig = () => setAddSourceStep(AddSourceSteps.SaveConfigStep); const setConfigCompletedStep = () => setAddSourceStep(AddSourceSteps.ConfigCompletedStep); const goToConfigCompleted = () => saveSourceConfig(false, setConfigCompletedStep); + const goToChoice = () => setAddSourceStep(AddSourceSteps.ChoiceStep); const FORM_SOURCE_ADDED_SUCCESS_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.contentSource.formSourceAddedSuccessMessage', { @@ -75,7 +79,11 @@ export const AddSource: React.FC = (props) => { return ( {addSourceCurrentStep === AddSourceSteps.ConfigIntroStep && ( - + )} {addSourceCurrentStep === AddSourceSteps.SaveConfigStep && ( = (props) => { {addSourceCurrentStep === AddSourceSteps.ReauthenticateStep && ( )} + {addSourceCurrentStep === AddSourceSteps.ChoiceStep && ( + + )} ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts index db0c5b97372636..92fab713a3fa09 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts @@ -41,6 +41,7 @@ export enum AddSourceSteps { ConnectInstanceStep = 'Connect Instance', ConfigureOauthStep = 'Configure Oauth', ReauthenticateStep = 'Reauthenticate', + ChoiceStep = 'Choice', } export interface OauthParams { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.test.tsx index bfb916847d865e..392ce175d271db 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.test.tsx @@ -13,10 +13,6 @@ import { shallow } from 'enzyme'; import { EuiText, EuiButton } from '@elastic/eui'; -import { - PersonalDashboardLayout, - WorkplaceSearchPageTemplate, -} from '../../../../components/layout'; import { staticSourceData } from '../../source_data'; import { ConfigurationChoice } from './configuration_choice'; @@ -35,22 +31,6 @@ describe('ConfigurationChoice', () => { jest.clearAllMocks(); }); - describe('layout', () => { - it('renders the default workplace search layout when on an organization view', () => { - setMockValues({ ...mockValues, isOrganization: true }); - const wrapper = shallow(); - - expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate); - }); - - it('renders the personal dashboard layout when not in an organization', () => { - setMockValues({ ...mockValues, isOrganization: false }); - const wrapper = shallow(); - - expect(wrapper.type()).toEqual(PersonalDashboardLayout); - }); - }); - it('renders internal connector if available', () => { const wrapper = shallow(); @@ -64,6 +44,16 @@ describe('ConfigurationChoice', () => { button.simulate('click'); expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/box/internal/'); }); + it('should call prop function when provided on internal connector click', () => { + const advanceSpy = jest.fn(); + const wrapper = shallow( + + ); + const button = wrapper.find(EuiButton); + button.simulate('click'); + expect(navigateToUrl).not.toHaveBeenCalled(); + expect(advanceSpy).toHaveBeenCalled(); + }); it('renders external connector if available', () => { const wrapper = shallow( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.tsx index 46a8998c9dd10a..f5d6d51651dd4d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.tsx @@ -9,16 +9,11 @@ import React from 'react'; import { useValues } from 'kea'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { KibanaLogic } from '../../../../../shared/kibana'; import { AppLogic } from '../../../../app_logic'; -import { - WorkplaceSearchPageTemplate, - PersonalDashboardLayout, -} from '../../../../components/layout'; -import { NAV } from '../../../../constants'; import { getAddPath, getSourcesPath } from '../../../../routes'; import { SourceDataItem } from '../../../../types'; @@ -26,6 +21,7 @@ import { AddSourceHeader } from './add_source_header'; interface ConfigurationIntroProps { sourceData: SourceDataItem; + goToInternalStep?: () => void; } export const ConfigurationChoice: React.FC = ({ @@ -36,15 +32,18 @@ export const ConfigurationChoice: React.FC = ({ internalConnectorAvailable, customConnectorAvailable, }, + goToInternalStep, }) => { const { isOrganization } = useValues(AppLogic); - const goToInternal = () => - KibanaLogic.values.navigateToUrl( - `${getSourcesPath( - `${getSourcesPath(getAddPath(serviceType), isOrganization)}/internal`, - isOrganization - )}/` - ); + const goToInternal = goToInternalStep + ? goToInternalStep + : () => + KibanaLogic.values.navigateToUrl( + `${getSourcesPath( + `${getSourcesPath(getAddPath(serviceType), isOrganization)}/internal`, + isOrganization + )}/` + ); const goToExternal = () => KibanaLogic.values.navigateToUrl( `${getSourcesPath( @@ -59,12 +58,10 @@ export const ConfigurationChoice: React.FC = ({ isOrganization )}/` ); - const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout; return ( - + <> - = ({ )} - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx index c2cd58a90f209d..e735119f687cc1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx @@ -90,9 +90,10 @@ export const SourcesRouter: React.FC = () => { : externalConnectorAvailable ? 'external' : 'custom'; + const showChoice = defaultOption !== 'internal' && hasMultipleConnectorOptions(sourceData); return ( - {hasMultipleConnectorOptions(sourceData) ? ( + {showChoice ? ( ) : ( From ccf16e99f990075738e3c04ee90e8eb958135fc5 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Mon, 7 Mar 2022 11:40:57 -0500 Subject: [PATCH 18/19] [Security Solution] Add metadta doc and capabilities to telemetry (#126680) --- .../server/lib/telemetry/__mocks__/index.ts | 1 + .../server/lib/telemetry/receiver.ts | 54 ++++++++++++++++++ .../server/lib/telemetry/tasks/endpoint.ts | 57 ++++++++++++++++--- .../server/lib/telemetry/types.ts | 38 +++++++++++++ 4 files changed, 143 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts b/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts index 4d9b67af6c311c..e18d104b0d73ac 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts @@ -44,6 +44,7 @@ export const createMockTelemetryReceiver = ( fetchTrustedApplications: jest.fn(), fetchEndpointList: jest.fn(), fetchDetectionRules: jest.fn().mockReturnValue({ body: null }), + fetchEndpointMetadata: jest.fn(), } as unknown as jest.Mocked; }; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts index 6e24cea41b7187..91054577656b13 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts @@ -80,6 +80,13 @@ export interface ITelemetryReceiver { TransportResult>, unknown> >; + fetchEndpointMetadata( + executeFrom: string, + executeTo: string + ): Promise< + TransportResult>, unknown> + >; + fetchDiagnosticAlerts( executeFrom: string, executeTo: string @@ -270,6 +277,53 @@ export class TelemetryReceiver implements ITelemetryReceiver { return this.esClient.search(query, { meta: true }); } + public async fetchEndpointMetadata(executeFrom: string, executeTo: string) { + if (this.esClient === undefined || this.esClient === null) { + throw Error('elasticsearch client is unavailable: cannot retrieve elastic endpoint metrics'); + } + + const query: SearchRequest = { + expand_wildcards: ['open' as const, 'hidden' as const], + index: `.ds-metrics-endpoint.metadata-*`, + ignore_unavailable: false, + size: 0, // no query results required - only aggregation quantity + body: { + query: { + range: { + '@timestamp': { + gte: executeFrom, + lt: executeTo, + }, + }, + }, + aggs: { + endpoint_metadata: { + terms: { + field: 'agent.id', + size: this.max_records, + }, + aggs: { + latest_metadata: { + top_hits: { + size: 1, + sort: [ + { + '@timestamp': { + order: 'desc' as const, + }, + }, + ], + }, + }, + }, + }, + }, + }, + }; + + return this.esClient.search(query, { meta: true }); + } + public async fetchDiagnosticAlerts(executeFrom: string, executeTo: string) { if (this.esClient === undefined || this.esClient === null) { throw Error('elasticsearch client is unavailable: cannot retrieve diagnostic alerts'); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts index e9cc36bbff907f..c2c318debccdaa 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts @@ -11,6 +11,8 @@ import type { EndpointMetricsAggregation, EndpointPolicyResponseAggregation, EndpointPolicyResponseDocument, + EndpointMetadataAggregation, + EndpointMetadataDocument, ESClusterInfo, ESLicense, } from '../types'; @@ -188,7 +190,36 @@ export function createTelemetryEndpointTaskConfig(maxTelemetryBatch: number) { ) : new Map(); - /** STAGE 4 - Create the telemetry log records + /** STAGE 4 - Fetch Endpoint Agent Metadata + * + * Reads Endpoint Agent metadata out of the `.ds-metrics-endpoint.metadata` data stream + * and buckets them by Endpoint Agent id and sorts by the top hit. The EP agent will + * report its metadata once per day OR every time a policy change has occured. If + * a metadata document(s) exists for an EP agent we map to fleet agent and policy + */ + if (endpointData.endpointMetadata === undefined) { + logger.debug(`no endpoint metadata to report`); + } + + const { body: endpointMetadataResponse } = endpointData.endpointMetadata as unknown as { + body: EndpointMetadataAggregation; + }; + + if (endpointMetadataResponse.aggregations === undefined) { + logger.debug(`no endpoint metadata to report`); + } + + const endpointMetadata = + endpointMetadataResponse.aggregations.endpoint_metadata.buckets.reduce( + (cache, endpointAgentId) => { + const doc = endpointAgentId.latest_metadata.hits.hits[0]; + cache.set(endpointAgentId.key, doc); + return cache; + }, + new Map() + ); + + /** STAGE 5 - Create the telemetry log records * * Iterates through the endpoint metrics documents at STAGE 1 and joins them together * to form the telemetry log that is sent back to Elastic Security developers to @@ -199,6 +230,7 @@ export function createTelemetryEndpointTaskConfig(maxTelemetryBatch: number) { const telemetryPayloads = endpointMetrics.map((endpoint) => { let policyConfig = null; let failedPolicy = null; + let endpointMetadataById = null; const fleetAgentId = endpoint.endpoint_metrics.elastic.agent.id; const endpointAgentId = endpoint.endpoint_agent; @@ -212,6 +244,10 @@ export function createTelemetryEndpointTaskConfig(maxTelemetryBatch: number) { } } + if (endpointMetadata) { + endpointMetadataById = endpointMetadata.get(endpointAgentId); + } + const { cpu, memory, @@ -242,6 +278,10 @@ export function createTelemetryEndpointTaskConfig(maxTelemetryBatch: number) { }, endpoint_meta: { os: endpoint.endpoint_metrics.host.os, + capabilities: + endpointMetadataById !== null && endpointMetadataById !== undefined + ? endpointMetadataById._source.Endpoint.capabilities + : [], }, policy_config: endpointPolicyDetail !== null ? endpointPolicyDetail : {}, policy_response: @@ -265,7 +305,7 @@ export function createTelemetryEndpointTaskConfig(maxTelemetryBatch: number) { }); /** - * STAGE 5 - Send the documents + * STAGE 6 - Send the documents * * Send the documents in a batches of maxTelemetryBatch */ @@ -287,11 +327,13 @@ async function fetchEndpointData( executeFrom: string, executeTo: string ) { - const [fleetAgentsResponse, epMetricsResponse, policyResponse] = await Promise.allSettled([ - receiver.fetchFleetAgents(), - receiver.fetchEndpointMetrics(executeFrom, executeTo), - receiver.fetchEndpointPolicyResponses(executeFrom, executeTo), - ]); + const [fleetAgentsResponse, epMetricsResponse, policyResponse, endpointMetadata] = + await Promise.allSettled([ + receiver.fetchFleetAgents(), + receiver.fetchEndpointMetrics(executeFrom, executeTo), + receiver.fetchEndpointPolicyResponses(executeFrom, executeTo), + receiver.fetchEndpointMetadata(executeFrom, executeTo), + ]); return { fleetAgentsResponse: @@ -300,5 +342,6 @@ async function fetchEndpointData( : EmptyFleetAgentResponse, endpointMetrics: epMetricsResponse.status === 'fulfilled' ? epMetricsResponse.value : undefined, epPolicyResponse: policyResponse.status === 'fulfilled' ? policyResponse.value : undefined, + endpointMetadata: endpointMetadata.status === 'fulfilled' ? endpointMetadata.value : undefined, }; } diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/types.ts b/x-pack/plugins/security_solution/server/lib/telemetry/types.ts index 35b531ae6941c3..c1c65a428f62d9 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/types.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/types.ts @@ -243,6 +243,44 @@ interface EndpointMetricOS { full: string; } +// EP Metadata + +export interface EndpointMetadataAggregation { + hits: { + total: { value: number }; + }; + aggregations: { + endpoint_metadata: { + buckets: Array<{ key: string; doc_count: number; latest_metadata: EndpointMetadataHits }>; + }; + }; +} + +interface EndpointMetadataHits { + hits: { + total: { value: number }; + hits: EndpointMetadataDocument[]; + }; +} + +export interface EndpointMetadataDocument { + _source: { + '@timestamp': string; + agent: { + id: string; + version: string; + }; + Endpoint: { + capabilities: string[]; + }; + elastic: { + agent: { + id: string; + }; + }; + }; +} + // List HTTP Types export const GetTrustedAppsRequestSchema = { From 8a8bfd56e72fcc763d536f400ed58e7eaf319abd Mon Sep 17 00:00:00 2001 From: Max Kovalev Date: Mon, 7 Mar 2022 18:42:58 +0200 Subject: [PATCH 19/19] [Maps] Fixed double click issue when deleting a shape (#124661) * #117369 - Fixed double click issue when deleting a shape * #117369 - updated deleting shape method; redux changes; * #117369 - waiting state for adding shapes feature * #1173669 - refactoring; removed unused functions * 117369 - refactoring * 117369 - Updates for onDraw methods, now the selected shape is not reset * 117369 - made addNewFeatureToIndex to be called once * 117369 - refactoring * 117369 - refactoring and clean up * 117369 - removed then method from actions * 117369 - refactoring * 1177369 - refactoring * 117369 - refactoring; Added new state in redux * 117369 - refactoring * 117369 - renaming layerId to featureId Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/maps/common/constants.ts | 1 + .../maps/public/actions/map_actions.ts | 26 ++++++++++++++++--- .../plugins/maps/public/actions/ui_actions.ts | 15 +++++++++++ .../mb_map/draw_control/draw_control.tsx | 21 +++------------ .../draw_feature_control.tsx | 14 ++++++---- .../draw_feature_control/index.ts | 9 +++---- .../draw_filter_control.tsx | 2 +- .../mb_map/draw_control/index.ts | 25 ------------------ x-pack/plugins/maps/public/reducers/ui.ts | 14 ++++++++++ .../maps/public/selectors/ui_selectors.ts | 1 + 10 files changed, 70 insertions(+), 58 deletions(-) delete mode 100644 x-pack/plugins/maps/public/connected_components/mb_map/draw_control/index.ts diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 6b9f32b92b9c19..1720f0ebdb5582 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -144,6 +144,7 @@ export enum DRAW_SHAPE { LINE = 'LINE', SIMPLE_SELECT = 'SIMPLE_SELECT', DELETE = 'DELETE', + WAIT = 'WAIT', } export const AGG_DELIMITER = '_of_'; diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index 2abbfc0d076a1e..cccb49f3606226 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -14,6 +14,7 @@ import turfBooleanContains from '@turf/boolean-contains'; import { Filter } from '@kbn/es-query'; import { Query, TimeRange } from 'src/plugins/data/public'; import { Geometry, Position } from 'geojson'; +import { asyncForEach } from '@kbn/std'; import { DRAW_MODE, DRAW_SHAPE } from '../../common/constants'; import type { MapExtentState, MapViewContext } from '../reducers/map/types'; import { MapStoreState } from '../reducers/store'; @@ -63,9 +64,10 @@ import { DrawState, MapCenterAndZoom, MapExtent, Timeslice } from '../../common/ import { INITIAL_LOCATION } from '../../common/constants'; import { updateTooltipStateForLayer } from './tooltip_actions'; import { isVectorLayer, IVectorLayer } from '../classes/layers/vector_layer'; -import { SET_DRAW_MODE } from './ui_actions'; +import { SET_DRAW_MODE, pushDeletedFeatureId, clearDeletedFeatureIds } from './ui_actions'; import { expandToTileBoundaries } from '../classes/util/geo_tile_utils'; import { getToasts } from '../kibana_services'; +import { getDeletedFeatureIds } from '../selectors/ui_selectors'; export function setMapInitError(errorMessage: string) { return { @@ -321,6 +323,10 @@ export function updateEditShape(shapeToDraw: DRAW_SHAPE | null) { drawShape: shapeToDraw, }, }); + + if (shapeToDraw !== DRAW_SHAPE.DELETE) { + dispatch(clearDeletedFeatureIds()); + } }; } @@ -353,7 +359,7 @@ export function updateEditLayer(layerId: string | null) { }; } -export function addNewFeatureToIndex(geometry: Geometry | Position[]) { +export function addNewFeatureToIndex(geometries: Array) { return async ( dispatch: ThunkDispatch, getState: () => MapStoreState @@ -369,7 +375,10 @@ export function addNewFeatureToIndex(geometry: Geometry | Position[]) { } try { - await (layer as IVectorLayer).addFeature(geometry); + dispatch(updateEditShape(DRAW_SHAPE.WAIT)); + await asyncForEach(geometries, async (geometry) => { + await (layer as IVectorLayer).addFeature(geometry); + }); await dispatch(syncDataForLayerDueToDrawing(layer)); } catch (e) { getToasts().addError(e, { @@ -378,6 +387,7 @@ export function addNewFeatureToIndex(geometry: Geometry | Position[]) { }), }); } + dispatch(updateEditShape(DRAW_SHAPE.SIMPLE_SELECT)); }; } @@ -386,6 +396,12 @@ export function deleteFeatureFromIndex(featureId: string) { dispatch: ThunkDispatch, getState: () => MapStoreState ) => { + // There is a race condition where users can click on a previously deleted feature before layer has re-rendered after feature delete. + // Check ensures delete requests for previously deleted features are aborted. + if (getDeletedFeatureIds(getState()).includes(featureId)) { + return; + } + const editState = getEditState(getState()); const layerId = editState ? editState.layerId : undefined; if (!layerId) { @@ -395,8 +411,11 @@ export function deleteFeatureFromIndex(featureId: string) { if (!layer || !isVectorLayer(layer)) { return; } + try { + dispatch(updateEditShape(DRAW_SHAPE.WAIT)); await (layer as IVectorLayer).deleteFeature(featureId); + dispatch(pushDeletedFeatureId(featureId)); await dispatch(syncDataForLayerDueToDrawing(layer)); } catch (e) { getToasts().addError(e, { @@ -405,5 +424,6 @@ export function deleteFeatureFromIndex(featureId: string) { }), }); } + dispatch(updateEditShape(DRAW_SHAPE.DELETE)); }; } diff --git a/x-pack/plugins/maps/public/actions/ui_actions.ts b/x-pack/plugins/maps/public/actions/ui_actions.ts index 70e24283ef48f7..1ffcf416f6f8fa 100644 --- a/x-pack/plugins/maps/public/actions/ui_actions.ts +++ b/x-pack/plugins/maps/public/actions/ui_actions.ts @@ -24,6 +24,8 @@ export const SET_OPEN_TOC_DETAILS = 'SET_OPEN_TOC_DETAILS'; export const SHOW_TOC_DETAILS = 'SHOW_TOC_DETAILS'; export const HIDE_TOC_DETAILS = 'HIDE_TOC_DETAILS'; export const SET_DRAW_MODE = 'SET_DRAW_MODE'; +export const PUSH_DELETED_FEATURE_ID = 'PUSH_DELETED_FEATURE_ID'; +export const CLEAR_DELETED_FEATURE_IDS = 'CLEAR_DELETED_FEATURE_IDS'; export function exitFullScreen() { return { @@ -123,3 +125,16 @@ export function closeTimeslider() { dispatch(setQuery({ clearTimeslice: true })); }; } + +export function pushDeletedFeatureId(featureId: string) { + return { + type: PUSH_DELETED_FEATURE_ID, + featureId, + }; +} + +export function clearDeletedFeatureIds() { + return { + type: CLEAR_DELETED_FEATURE_IDS, + }; +} diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx index a404db91a942ef..8cbfcd3a41e80e 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx @@ -20,15 +20,6 @@ import { DRAW_SHAPE } from '../../../../common/constants'; import { DrawCircle, DRAW_CIRCLE_RADIUS_LABEL_STYLE } from './draw_circle'; import { DrawTooltip } from './draw_tooltip'; -const mbModeEquivalencies = new Map([ - ['simple_select', DRAW_SHAPE.SIMPLE_SELECT], - ['draw_rectangle', DRAW_SHAPE.BOUNDS], - ['draw_circle', DRAW_SHAPE.DISTANCE], - ['draw_polygon', DRAW_SHAPE.POLYGON], - ['draw_line_string', DRAW_SHAPE.LINE], - ['draw_point', DRAW_SHAPE.POINT], -]); - const DRAW_RECTANGLE = 'draw_rectangle'; const DRAW_CIRCLE = 'draw_circle'; const mbDrawModes = MapboxDraw.modes; @@ -41,7 +32,6 @@ export interface Props { onClick?: (event: MapMouseEvent, drawControl?: MapboxDraw) => void; mbMap: MbMap; enable: boolean; - updateEditShape: (shapeToDraw: DRAW_SHAPE) => void; } export class DrawControl extends Component { @@ -91,12 +81,6 @@ export class DrawControl extends Component { } }, 0); - _onModeChange = ({ mode }: { mode: string }) => { - if (mbModeEquivalencies.has(mode)) { - this.props.updateEditShape(mbModeEquivalencies.get(mode)!); - } - }; - _removeDrawControl() { // Do not remove draw control after mbMap.remove is called, causes execeptions and mbMap.remove cleans up all map resources. const isMapRemoved = !this.props.mbMap.loaded(); @@ -105,7 +89,6 @@ export class DrawControl extends Component { } this.props.mbMap.getCanvas().style.cursor = ''; - this.props.mbMap.off('draw.modechange', this._onModeChange); this.props.mbMap.off('draw.create', this._onDraw); if (this.props.onClick) { this.props.mbMap.off('click', this._onClick); @@ -118,7 +101,6 @@ export class DrawControl extends Component { if (!this._mbDrawControlAdded) { this.props.mbMap.addControl(this._mbDrawControl); this._mbDrawControlAdded = true; - this.props.mbMap.on('draw.modechange', this._onModeChange); this.props.mbMap.on('draw.create', this._onDraw); if (this.props.onClick) { @@ -144,6 +126,9 @@ export class DrawControl extends Component { this._mbDrawControl.changeMode(DRAW_POINT); } else if (this.props.drawShape === DRAW_SHAPE.DELETE) { this._mbDrawControl.changeMode(SIMPLE_SELECT); + } else if (this.props.drawShape === DRAW_SHAPE.WAIT) { + this.props.mbMap.getCanvas().style.cursor = 'wait'; + this._mbDrawControl.changeMode(SIMPLE_SELECT); } else { this._mbDrawControl.changeMode(SIMPLE_SELECT); } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx index 6c7fe9f0ad213a..b6ffacc491030e 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx @@ -15,7 +15,7 @@ import { i18n } from '@kbn/i18n'; import * as jsts from 'jsts'; import { MapMouseEvent } from '@kbn/mapbox-gl'; import { getToasts } from '../../../../kibana_services'; -import { DrawControl } from '../'; +import { DrawControl } from '../draw_control'; import { DRAW_MODE, DRAW_SHAPE } from '../../../../../common/constants'; import { ILayer } from '../../../../classes/layers/layer'; import { EXCLUDE_CENTROID_FEATURES } from '../../../../classes/util/mb_filter_expressions'; @@ -29,9 +29,8 @@ export interface ReduxStateProps { } export interface ReduxDispatchProps { - addNewFeatureToIndex: (geometry: Geometry | Position[]) => void; + addNewFeatureToIndex: (geometries: Array) => void; deleteFeatureFromIndex: (featureId: string) => void; - disableDrawState: () => void; } export interface OwnProps { @@ -43,6 +42,7 @@ type Props = ReduxStateProps & ReduxDispatchProps & OwnProps; export class DrawFeatureControl extends Component { _onDraw = async (e: { features: Feature[] }, mbDrawControl: MapboxDraw) => { try { + const geometries: Array = []; e.features.forEach((feature: Feature) => { const { geometry } = geoJSONReader.read(feature); if (!geometry.isSimple() || !geometry.isValid()) { @@ -58,9 +58,13 @@ export class DrawFeatureControl extends Component { this.props.drawMode === DRAW_MODE.DRAW_POINTS ? feature.geometry.coordinates : feature.geometry; - this.props.addNewFeatureToIndex(featureGeom); + geometries.push(featureGeom); } }); + + if (geometries.length) { + this.props.addNewFeatureToIndex(geometries); + } } catch (error) { getToasts().addWarning( i18n.translate('xpack.maps.drawFeatureControl.unableToCreateFeature', { @@ -71,7 +75,6 @@ export class DrawFeatureControl extends Component { }) ); } finally { - this.props.disableDrawState(); try { mbDrawControl.deleteAll(); } catch (_e) { @@ -86,6 +89,7 @@ export class DrawFeatureControl extends Component { if (!this.props.editLayer || this.props.drawShape !== DRAW_SHAPE.DELETE) { return; } + const mbEditLayerIds = this.props.editLayer .getMbLayerIds() .filter((mbLayerId) => !!this.props.mbMap.getLayer(mbLayerId)); diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/index.ts index e1d703173fc2da..d2c369b4bd50a2 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/index.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/index.ts @@ -15,7 +15,7 @@ import { ReduxStateProps, OwnProps, } from './draw_feature_control'; -import { addNewFeatureToIndex, deleteFeatureFromIndex, updateEditShape } from '../../../../actions'; +import { addNewFeatureToIndex, deleteFeatureFromIndex } from '../../../../actions'; import { MapStoreState } from '../../../../reducers/store'; import { getEditState, getLayerById } from '../../../../selectors/map_selectors'; import { getDrawMode } from '../../../../selectors/ui_selectors'; @@ -34,15 +34,12 @@ function mapDispatchToProps( dispatch: ThunkDispatch ): ReduxDispatchProps { return { - addNewFeatureToIndex(geometry: Geometry | Position[]) { - dispatch(addNewFeatureToIndex(geometry)); + addNewFeatureToIndex(geometries: Array) { + dispatch(addNewFeatureToIndex(geometries)); }, deleteFeatureFromIndex(featureId: string) { dispatch(deleteFeatureFromIndex(featureId)); }, - disableDrawState() { - dispatch(updateEditShape(null)); - }, }; } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/draw_filter_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/draw_filter_control.tsx index 2f652506857d2a..98d88d43fc65f9 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/draw_filter_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/draw_filter_control.tsx @@ -20,7 +20,7 @@ import { roundCoordinates, } from '../../../../../common/elasticsearch_util'; import { getToasts } from '../../../../kibana_services'; -import { DrawControl } from '../'; +import { DrawControl } from '../draw_control'; import { DrawCircleProperties } from '../draw_circle'; export interface Props { diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/index.ts deleted file mode 100644 index b0f1941caec08c..00000000000000 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ThunkDispatch } from 'redux-thunk'; -import { AnyAction } from 'redux'; -import { connect } from 'react-redux'; -import { updateEditShape } from '../../../actions'; -import { MapStoreState } from '../../../reducers/store'; -import { DrawControl } from './draw_control'; -import { DRAW_SHAPE } from '../../../../common/constants'; - -function mapDispatchToProps(dispatch: ThunkDispatch) { - return { - updateEditShape(shapeToDraw: DRAW_SHAPE) { - dispatch(updateEditShape(shapeToDraw)); - }, - }; -} - -const connected = connect(null, mapDispatchToProps)(DrawControl); -export { connected as DrawControl }; diff --git a/x-pack/plugins/maps/public/reducers/ui.ts b/x-pack/plugins/maps/public/reducers/ui.ts index f3f948bb96508a..f0f22c5a8c4a93 100644 --- a/x-pack/plugins/maps/public/reducers/ui.ts +++ b/x-pack/plugins/maps/public/reducers/ui.ts @@ -19,6 +19,8 @@ import { SHOW_TOC_DETAILS, HIDE_TOC_DETAILS, SET_DRAW_MODE, + PUSH_DELETED_FEATURE_ID, + CLEAR_DELETED_FEATURE_IDS, } from '../actions'; import { DRAW_MODE } from '../../common/constants'; @@ -37,6 +39,7 @@ export type MapUiState = { isLayerTOCOpen: boolean; isTimesliderOpen: boolean; openTOCDetails: string[]; + deletedFeatureIds: string[]; }; export const DEFAULT_IS_LAYER_TOC_OPEN = true; @@ -51,6 +54,7 @@ export const DEFAULT_MAP_UI_STATE = { // storing TOC detail visibility outside of map.layerList because its UI state and not map rendering state. // This also makes for easy read/write access for embeddables. openTOCDetails: [], + deletedFeatureIds: [], }; // Reducer @@ -82,6 +86,16 @@ export function ui(state: MapUiState = DEFAULT_MAP_UI_STATE, action: any) { return layerId !== action.layerId; }), }; + case PUSH_DELETED_FEATURE_ID: + return { + ...state, + deletedFeatureIds: [...state.deletedFeatureIds, action.featureId], + }; + case CLEAR_DELETED_FEATURE_IDS: + return { + ...state, + deletedFeatureIds: [], + }; default: return state; } diff --git a/x-pack/plugins/maps/public/selectors/ui_selectors.ts b/x-pack/plugins/maps/public/selectors/ui_selectors.ts index 942a5190691a15..6bdf5a35679a73 100644 --- a/x-pack/plugins/maps/public/selectors/ui_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/ui_selectors.ts @@ -17,3 +17,4 @@ export const getIsTimesliderOpen = ({ ui }: MapStoreState): boolean => ui.isTime export const getOpenTOCDetails = ({ ui }: MapStoreState): string[] => ui.openTOCDetails; export const getIsFullScreen = ({ ui }: MapStoreState): boolean => ui.isFullScreen; export const getIsReadOnly = ({ ui }: MapStoreState): boolean => ui.isReadOnly; +export const getDeletedFeatureIds = ({ ui }: MapStoreState): string[] => ui.deletedFeatureIds;