From 5890a0189663d7d6b21d80a8b86cd89381177175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Thu, 21 Oct 2021 16:58:37 +0200 Subject: [PATCH 01/40] [Logs UI] [Metrics UI] Register deprecations for the logAlias and metricAlias config settings (#115845) --- x-pack/plugins/infra/server/deprecations.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/infra/server/deprecations.ts b/x-pack/plugins/infra/server/deprecations.ts index 27c2b235f769b..70131cd96d117 100644 --- a/x-pack/plugins/infra/server/deprecations.ts +++ b/x-pack/plugins/infra/server/deprecations.ts @@ -142,7 +142,7 @@ const FIELD_DEPRECATION_FACTORIES: Record Dep }), }; -export const configDeprecations: ConfigDeprecationProvider = () => [ +export const configDeprecations: ConfigDeprecationProvider = ({ deprecate }) => [ ...Object.keys(FIELD_DEPRECATION_FACTORIES).map( (key): ConfigDeprecation => (completeConfig, rootPath, addDeprecation) => { @@ -179,6 +179,8 @@ export const configDeprecations: ConfigDeprecationProvider = () => [ return completeConfig; } ), + deprecate('sources.default.logAlias', '8.0.0'), + deprecate('sources.default.metricAlias', '8.0.0'), ]; export const getInfraDeprecationsFactory = From 729481ed5986ffd721f1871a6bb33620858cf443 Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Thu, 21 Oct 2021 17:11:24 +0200 Subject: [PATCH 02/40] [ML] Functional tests - stabilize categorization examples endpoint test (#115910) This PR stabilizes the categorization example endpoint tests by using a different esArchive. --- x-pack/test/api_integration/apis/ml/index.ts | 1 + .../ml/jobs/categorization_field_examples.ts | 4 +- .../ml/categorization_small/data.json.gz | Bin 0 -> 56907 bytes .../ml/categorization_small/mappings.json | 41 ++++++++++++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 x-pack/test/functional/es_archives/ml/categorization_small/data.json.gz create mode 100644 x-pack/test/functional/es_archives/ml/categorization_small/mappings.json diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index e44d0cd10e9f2..9b530873ad165 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -44,6 +44,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await esArchiver.unload('x-pack/test/functional/es_archives/ml/ecommerce'); await esArchiver.unload('x-pack/test/functional/es_archives/ml/categorization'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/categorization_small'); await esArchiver.unload('x-pack/test/functional/es_archives/ml/module_apache'); await esArchiver.unload('x-pack/test/functional/es_archives/ml/module_auditbeat'); await esArchiver.unload('x-pack/test/functional/es_archives/ml/module_apm'); diff --git a/x-pack/test/api_integration/apis/ml/jobs/categorization_field_examples.ts b/x-pack/test/api_integration/apis/ml/jobs/categorization_field_examples.ts index 4686787ae9b16..9d6009bbb3ea6 100644 --- a/x-pack/test/api_integration/apis/ml/jobs/categorization_field_examples.ts +++ b/x-pack/test/api_integration/apis/ml/jobs/categorization_field_examples.ts @@ -64,7 +64,7 @@ const analyzer = { ], }; const defaultRequestBody = { - indexPatternTitle: 'ft_categorization', + indexPatternTitle: 'ft_categorization_small', query: { bool: { must: [{ match_all: {} }] } }, size: 5, timeField: '@timestamp', @@ -286,7 +286,7 @@ export default ({ getService }: FtrProviderContext) => { describe('Categorization example endpoint - ', function () { before(async () => { - await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/categorization'); + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/categorization_small'); await ml.testResources.setKibanaTimeZoneToUTC(); }); diff --git a/x-pack/test/functional/es_archives/ml/categorization_small/data.json.gz b/x-pack/test/functional/es_archives/ml/categorization_small/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..76ac07831dec16d64a3ebb4829acb70c2e8fb98e GIT binary patch literal 56907 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 literal 0 HcmV?d00001 diff --git a/x-pack/test/functional/es_archives/ml/categorization_small/mappings.json b/x-pack/test/functional/es_archives/ml/categorization_small/mappings.json new file mode 100644 index 0000000000000..b73babf361625 --- /dev/null +++ b/x-pack/test/functional/es_archives/ml/categorization_small/mappings.json @@ -0,0 +1,41 @@ +{ + "type": "index", + "value": { + "aliases": {}, + "index": "ft_categorization_small", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "field1": { + "type": "text" + }, + "field2": { + "type": "text" + }, + "field3": { + "type": "text" + }, + "field4": { + "type": "text" + }, + "field5": { + "type": "text" + }, + "field6": { + "type": "text" + }, + "field7": { + "type": "text" + } + } + }, + "settings": { + "index": { + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} From 31e7428f4956303102e7455eb48818c51eecaf68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Thu, 21 Oct 2021 17:22:46 +0200 Subject: [PATCH 03/40] [Index Management] Forward port of the fix for indices table test in 7.16 (#115933) --- .../__snapshots__/index_table.test.js.snap | 2 +- .../__jest__/components/index_table.test.js | 32 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/index_management/__jest__/components/__snapshots__/index_table.test.js.snap b/x-pack/plugins/index_management/__jest__/components/__snapshots__/index_table.test.js.snap index b35f3515a9af0..f4f886dd7211c 100644 --- a/x-pack/plugins/index_management/__jest__/components/__snapshots__/index_table.test.js.snap +++ b/x-pack/plugins/index_management/__jest__/components/__snapshots__/index_table.test.js.snap @@ -22,7 +22,7 @@ exports[`index table force merge button works from context menu 3`] = `"open"`; exports[`index table open index button works from context menu 1`] = `"opening..."`; -exports[`index table open index button works from context menu 2`] = `"opening..."`; +exports[`index table open index button works from context menu 2`] = `"open"`; exports[`index table refresh button works from context menu 1`] = `"refreshing..."`; diff --git a/x-pack/plugins/index_management/__jest__/components/index_table.test.js b/x-pack/plugins/index_management/__jest__/components/index_table.test.js index 808c44ddecce0..5e5538fcca4e8 100644 --- a/x-pack/plugins/index_management/__jest__/components/index_table.test.js +++ b/x-pack/plugins/index_management/__jest__/components/index_table.test.js @@ -88,6 +88,14 @@ const snapshot = (rendered) => { expect(rendered).toMatchSnapshot(); }; +const names = (rendered) => { + return findTestSubject(rendered, 'indexTableIndexNameLink'); +}; + +const namesText = (rendered) => { + return names(rendered).map((button) => button.text()); +}; + const openMenuAndClickButton = (rendered, rowIndex, buttonSelector) => { // Select a row. const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox'); @@ -111,7 +119,8 @@ const testEditor = (rendered, buttonSelector, rowIndex = 0) => { snapshot(findTestSubject(rendered, 'detailPanelTabSelected').text()); }; -const testAction = (rendered, buttonSelector, rowIndex = 0) => { +const testAction = (rendered, buttonSelector, indexName = 'testy0') => { + const rowIndex = namesText(rendered).indexOf(indexName); // This is leaking some implementation details about how Redux works. Not sure exactly what's going on // but it looks like we're aware of how many Redux actions are dispatched in response to user interaction, // so we "time" our assertion based on how many Redux actions we observe. This is brittle because it @@ -132,14 +141,6 @@ const testAction = (rendered, buttonSelector, rowIndex = 0) => { snapshot(status(rendered, rowIndex)); }; -const names = (rendered) => { - return findTestSubject(rendered, 'indexTableIndexNameLink'); -}; - -const namesText = (rendered) => { - return names(rendered).map((button) => button.text()); -}; - const getActionMenuButtons = (rendered) => { return findTestSubject(rendered, 'indexContextMenu') .find('button') @@ -487,24 +488,23 @@ describe('index table', () => { }); test('open index button works from context menu', async () => { - const rendered = mountWithIntl(component); - await runAllPromises(); - rendered.update(); - const modifiedIndices = indices.map((index) => { return { ...index, - status: index.name === 'testy1' ? 'open' : index.status, + status: index.name === 'testy1' ? 'closed' : index.status, }; }); - server.respondWith(`${API_BASE_PATH}/indices/reload`, [ + server.respondWith(`${API_BASE_PATH}/indices`, [ 200, { 'Content-Type': 'application/json' }, JSON.stringify(modifiedIndices), ]); - testAction(rendered, 'openIndexMenuButton', 1); + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + testAction(rendered, 'openIndexMenuButton', 'testy1'); }); test('show settings button works from context menu', async () => { From 079fbce79d7d5e9482ce3fb10f9ae63ef2a66a61 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Thu, 21 Oct 2021 18:26:28 +0300 Subject: [PATCH 04/40] [CodeEditor] add support of triple quotes (#112656) * [CodeEditor] add support of triple quotes * add tests for grammar * an escaped quote can be appended to the end of triple quotes' Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-monaco/src/xjson/grammar.test.ts | 189 ++++++++++++++++++ packages/kbn-monaco/src/xjson/grammar.ts | 79 ++++---- .../kbn-monaco/src/xjson/lexer_rules/xjson.ts | 1 + .../search/aggs/param_types/json.test.ts | 32 ++- .../common/search/aggs/param_types/json.ts | 14 +- src/plugins/vis_default_editor/kibana.json | 2 +- .../public/components/controls/raw_json.tsx | 3 +- 7 files changed, 274 insertions(+), 46 deletions(-) create mode 100644 packages/kbn-monaco/src/xjson/grammar.test.ts diff --git a/packages/kbn-monaco/src/xjson/grammar.test.ts b/packages/kbn-monaco/src/xjson/grammar.test.ts new file mode 100644 index 0000000000000..29d338cd71b0c --- /dev/null +++ b/packages/kbn-monaco/src/xjson/grammar.test.ts @@ -0,0 +1,189 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { createParser } from './grammar'; + +describe('createParser', () => { + let parser: ReturnType; + + beforeEach(() => { + parser = createParser(); + }); + + test('should create a xjson grammar parser', () => { + expect(createParser()).toBeInstanceOf(Function); + }); + + test('should return no annotations in case of valid json', () => { + expect( + parser(` + {"menu": { + "id": "file", + "value": "File", + "quotes": "'\\"", + "popup": { + "actions": [ + "new", + "open", + "close" + ], + "menuitem": [ + {"value": "New"}, + {"value": "Open"}, + {"value": "Close"} + ] + } + }} + `) + ).toMatchInlineSnapshot(` + Object { + "annotations": Array [], + } + `); + }); + + test('should support triple quotes', () => { + expect( + parser(` + {"menu": { + "id": """ + file + """, + "value": "File" + }} + `) + ).toMatchInlineSnapshot(` + Object { + "annotations": Array [], + } + `); + }); + + test('triple quotes should be correctly closed', () => { + expect( + parser(` + {"menu": { + "id": """" + file + "", + "value": "File" + }} + `) + ).toMatchInlineSnapshot(` + Object { + "annotations": Array [ + Object { + "at": 36, + "text": "Expected ',' instead of '\\"'", + "type": "error", + }, + ], + } + `); + }); + + test('an escaped quote can be appended to the end of triple quotes', () => { + expect( + parser(` + {"menu": { + "id": """ + file + \\"""", + "value": "File" + }} + `) + ).toMatchInlineSnapshot(` + Object { + "annotations": Array [], + } + `); + }); + + test('text values should be wrapper into quotes', () => { + expect( + parser(` + {"menu": { + "id": id, + "value": "File" + }} + `) + ).toMatchInlineSnapshot(` + Object { + "annotations": Array [ + Object { + "at": 36, + "text": "Unexpected 'i'", + "type": "error", + }, + ], + } + `); + }); + + test('check for close quotes', () => { + expect( + parser(` + {"menu": { + "id": "id, + "value": "File" + }} + `) + ).toMatchInlineSnapshot(` + Object { + "annotations": Array [ + Object { + "at": 52, + "text": "Expected ',' instead of 'v'", + "type": "error", + }, + ], + } + `); + }); + test('no duplicate keys', () => { + expect( + parser(` + {"menu": { + "id": "id", + "id": "File" + }} + `) + ).toMatchInlineSnapshot(` + Object { + "annotations": Array [ + Object { + "at": 53, + "text": "Duplicate key \\"id\\"", + "type": "warning", + }, + ], + } + `); + }); + + test('all curly quotes should be closed', () => { + expect( + parser(` + {"menu": { + "id": "id", + "name": "File" + } + `) + ).toMatchInlineSnapshot(` + Object { + "annotations": Array [ + Object { + "at": 82, + "text": "Expected ',' instead of ''", + "type": "error", + }, + ], + } + `); + }); +}); diff --git a/packages/kbn-monaco/src/xjson/grammar.ts b/packages/kbn-monaco/src/xjson/grammar.ts index 32c958e66d594..5d26e92f005ba 100644 --- a/packages/kbn-monaco/src/xjson/grammar.ts +++ b/packages/kbn-monaco/src/xjson/grammar.ts @@ -57,10 +57,6 @@ export const createParser = () => { text: m, }); }, - reset = function (newAt: number) { - ch = text.charAt(newAt); - at = newAt + 1; - }, next = function (c?: string) { return ( c && c !== ch && error("Expected '" + c + "' instead of '" + ch + "'"), @@ -69,15 +65,6 @@ export const createParser = () => { ch ); }, - nextUpTo = function (upTo: any, errorMessage: string) { - let currentAt = at, - i = text.indexOf(upTo, currentAt); - if (i < 0) { - error(errorMessage || "Expected '" + upTo + "'"); - } - reset(i + upTo.length); - return text.substring(currentAt, i); - }, peek = function (c: string) { return text.substr(at, c.length) === c; // nocommit - double check }, @@ -96,37 +83,50 @@ export const createParser = () => { (string += ch), next(); return (number = +string), isNaN(number) ? (error('Bad number'), void 0) : number; }, + stringLiteral = function () { + let quotes = '"""'; + let end = text.indexOf('\\"' + quotes, at + quotes.length); + + if (end >= 0) { + quotes = '\\"' + quotes; + } else { + end = text.indexOf(quotes, at + quotes.length); + } + + if (end >= 0) { + for (let l = end - at + quotes.length; l > 0; l--) { + next(); + } + } + + return next(); + }, string = function () { let hex: any, i: any, uffff: any, string = ''; + if ('"' === ch) { - if (peek('""')) { - // literal - next('"'); - next('"'); - return nextUpTo('"""', 'failed to find closing \'"""\''); - } else { - for (; next(); ) { - if ('"' === ch) return next(), string; - if ('\\' === ch) - if ((next(), 'u' === ch)) { - for ( - uffff = 0, i = 0; - 4 > i && ((hex = parseInt(next(), 16)), isFinite(hex)); - i += 1 - ) - uffff = 16 * uffff + hex; - string += String.fromCharCode(uffff); - } else { - if ('string' != typeof escapee[ch]) break; - string += escapee[ch]; - } - else string += ch; - } + for (; next(); ) { + if ('"' === ch) return next(), string; + if ('\\' === ch) + if ((next(), 'u' === ch)) { + for ( + uffff = 0, i = 0; + 4 > i && ((hex = parseInt(next(), 16)), isFinite(hex)); + i += 1 + ) + uffff = 16 * uffff + hex; + string += String.fromCharCode(uffff); + } else { + if ('string' != typeof escapee[ch]) break; + string += escapee[ch]; + } + else string += ch; } } + error('Bad string'); }, white = function () { @@ -165,9 +165,9 @@ export const createParser = () => { ((key = string()), white(), next(':'), - Object.hasOwnProperty.call(object, key) && + Object.hasOwnProperty.call(object, key!) && warning('Duplicate key "' + key + '"', latchKeyStart), - (object[key] = value()), + (object[key!] = value()), white(), '}' === ch) ) @@ -179,6 +179,9 @@ export const createParser = () => { }; return ( (value = function () { + if (peek('"""')) { + return stringLiteral(); + } switch ((white(), ch)) { case '{': return object(); diff --git a/packages/kbn-monaco/src/xjson/lexer_rules/xjson.ts b/packages/kbn-monaco/src/xjson/lexer_rules/xjson.ts index 2c8186ac7fa4f..f2ab22f8c97df 100644 --- a/packages/kbn-monaco/src/xjson/lexer_rules/xjson.ts +++ b/packages/kbn-monaco/src/xjson/lexer_rules/xjson.ts @@ -103,6 +103,7 @@ export const lexerRules: monaco.languages.IMonarchLanguage = { string_literal: [ [/"""/, { token: 'punctuation.end_triple_quote', next: '@pop' }], + [/\\""""/, { token: 'punctuation.end_triple_quote', next: '@pop' }], [/./, { token: 'multi_string' }], ], }, diff --git a/src/plugins/data/common/search/aggs/param_types/json.test.ts b/src/plugins/data/common/search/aggs/param_types/json.test.ts index 1b3af5b92c26b..8e71cf4657e1f 100644 --- a/src/plugins/data/common/search/aggs/param_types/json.test.ts +++ b/src/plugins/data/common/search/aggs/param_types/json.test.ts @@ -67,10 +67,34 @@ describe('JSON', function () { aggParam.write(aggConfig, output); expect(aggConfig.params).toHaveProperty(paramName); - expect(output.params).toEqual({ - existing: 'true', - new_param: 'should exist in output', - }); + expect(output.params).toMatchInlineSnapshot(` + Object { + "existing": "true", + "new_param": "should exist in output", + } + `); + }); + + it('should append param when valid JSON with triple quotes', () => { + const aggParam = initAggParam(); + const jsonData = `{ + "a": """ + multiline string - line 1 + """ + }`; + + aggConfig.params[paramName] = jsonData; + + aggParam.write(aggConfig, output); + expect(aggConfig.params).toHaveProperty(paramName); + + expect(output.params).toMatchInlineSnapshot(` + Object { + "a": " + multiline string - line 1 + ", + } + `); }); it('should not overwrite existing params', () => { diff --git a/src/plugins/data/common/search/aggs/param_types/json.ts b/src/plugins/data/common/search/aggs/param_types/json.ts index 1678b6586ce80..f499286140af1 100644 --- a/src/plugins/data/common/search/aggs/param_types/json.ts +++ b/src/plugins/data/common/search/aggs/param_types/json.ts @@ -11,6 +11,17 @@ import _ from 'lodash'; import { IAggConfig } from '../agg_config'; import { BaseParamType } from './base'; +function collapseLiteralStrings(xjson: string) { + const tripleQuotes = '"""'; + const splitData = xjson.split(tripleQuotes); + + for (let idx = 1; idx < splitData.length - 1; idx += 2) { + splitData[idx] = JSON.stringify(splitData[idx]); + } + + return splitData.join(''); +} + export class JsonParamType extends BaseParamType { constructor(config: Record) { super(config); @@ -26,9 +37,8 @@ export class JsonParamType extends BaseParamType { return; } - // handle invalid Json input try { - paramJson = JSON.parse(param); + paramJson = JSON.parse(collapseLiteralStrings(param)); } catch (err) { return; } diff --git a/src/plugins/vis_default_editor/kibana.json b/src/plugins/vis_default_editor/kibana.json index efed1eab1e494..253edc74f87b4 100644 --- a/src/plugins/vis_default_editor/kibana.json +++ b/src/plugins/vis_default_editor/kibana.json @@ -3,7 +3,7 @@ "version": "kibana", "ui": true, "optionalPlugins": ["visualize"], - "requiredBundles": ["kibanaUtils", "kibanaReact", "data", "fieldFormats", "discover"], + "requiredBundles": ["kibanaUtils", "kibanaReact", "data", "fieldFormats", "discover", "esUiShared"], "owner": { "name": "Vis Editors", "githubTeam": "kibana-vis-editors" diff --git a/src/plugins/vis_default_editor/public/components/controls/raw_json.tsx b/src/plugins/vis_default_editor/public/components/controls/raw_json.tsx index af6096be87f59..6e5ae78e54dc1 100644 --- a/src/plugins/vis_default_editor/public/components/controls/raw_json.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/raw_json.tsx @@ -12,6 +12,7 @@ import { EuiFormRow, EuiIconTip, EuiScreenReaderOnly } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { XJsonLang } from '@kbn/monaco'; import { CodeEditor } from '../../../../kibana_react/public'; +import { XJson } from '../../../../es_ui_shared/public'; import { AggParamEditorProps } from '../agg_param_props'; @@ -58,7 +59,7 @@ function RawJsonParamEditor({ let isJsonValid = true; try { if (newValue) { - JSON.parse(newValue); + JSON.parse(XJson.collapseLiteralStrings(newValue)); } } catch (e) { isJsonValid = false; From 7337a67c5317852f3fe6f510ae452184bcfbff80 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 21 Oct 2021 09:34:54 -0600 Subject: [PATCH 05/40] Fixes backwards compatibility with rule exports (#115888) ## Summary We at least want to do our best to retain n-1 version compatible with exports and then best effort for n-2 or more. During testing I saw that we changed our export contract and I was getting these errors importing from 7.15 into 7.16: Screen Shot 2021-10-20 at 8 25 07 PM The reason is because it thinks the final "summary line" which has `exported_count` instead of `exported_rules_count` will think that is a rule based ndjson line and not exclude it. This was introduced in this PR: https://github.com/elastic/kibana/pull/115144 I was under the impression that was adding more information and not subtracting information when I did the PR review. I almost think that we should keep the `exported_count` in addition to the new lines that are added so the user has a total count of everything exported. This PR doesn't do that, it just fixes the backwards compatibility to not have errors. If we ship like this the other rules from 7.15 and previous will still import but users will see error messages like that one above which they should never see. However, really, we should be careful about changes that are not just additive with formats. ### Checklist No this PR currently does not have tests for backwards compatibility but I think we should add it either here or a follow up. - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../detection_engine/rules/create_rules_stream_from_ndjson.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts index d4357c45fd373..799412a33ffbc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts @@ -24,6 +24,7 @@ import { filterExportedRulesCounts, filterExceptions, createLimitStream, + filterExportedCounts, } from '../../../utils/read_stream/create_stream_from_ndjson'; export const validateRules = (): Transform => { @@ -60,6 +61,7 @@ export const createRulesStreamFromNdJson = (ruleLimit: number) => { return [ createSplitStream('\n'), parseNdjsonStrings(), + filterExportedCounts(), filterExportedRulesCounts(), filterExceptions(), validateRules(), From 09d4aa6ca02bf236cc41dc25d26dfba9a4700f19 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 21 Oct 2021 09:36:36 -0600 Subject: [PATCH 06/40] Removes logger errors that are not needed (#115866) ## Summary When moving the saved object references I added logger errors and utilities to detect when the original data structure id changed from the saved object reference id's. However, when things move to mulit-space or if users upload different id's within these structures from something such as the saved object management system then users will see these errors in their logs when they shouldn't. I removed them all as this is expected behavior that the references array can and will contain different id's. Before you would see these messages on an upgrade to 8.0.0 or if you uploaded or changed a rule id through the SOM: ``` [2021-10-18T17:36:03.550-06:00][ERROR][plugins.securitySolution] The id of the "saved object reference id": ec94193b-4788-5ceb-b8fb-5e270836beec is not the same as the "saved object id": 6caeff30-3069-11ec-83d2-0376356fe525. Preferring and using the "saved object reference id" instead of the "saved object id" ``` Now you will not. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../legacy_inject_rule_id_references.test.ts | 11 ------ .../legacy_inject_rule_id_references.ts | 13 ------- .../inject_exceptions_list.test.ts | 11 ------ .../inject_exceptions_list.ts | 11 +----- .../saved_object_references/utils/index.ts | 1 - ...g_if_different_references_detected.test.ts | 38 ------------------- ...arning_if_different_references_detected.ts | 36 ------------------ 7 files changed, 1 insertion(+), 120 deletions(-) delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.test.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.ts diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_rule_id_references.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_rule_id_references.test.ts index 2f63a184875f1..f28d78e5c0304 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_rule_id_references.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_rule_id_references.test.ts @@ -77,17 +77,6 @@ describe('legacy_inject_rule_id_references', () => { expect(logger.error).not.toHaveBeenCalled(); }); - test('logs an error if found with a different saved object reference id', () => { - legacyInjectRuleIdReferences({ - logger, - ruleAlertId: '456', - savedObjectReferences: mockSavedObjectReferences(), - }); - expect(logger.error).toBeCalledWith( - 'The id of the "saved object reference id": 123 is not the same as the "saved object id": 456. Preferring and using the "saved object reference id" instead of the "saved object id"' - ); - }); - test('logs an error if the saved object references is empty', () => { legacyInjectRuleIdReferences({ logger, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_rule_id_references.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_rule_id_references.ts index 5cb32c6563157..b6ad98eb864ed 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_rule_id_references.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_saved_object_references/legacy_inject_rule_id_references.ts @@ -32,19 +32,6 @@ export const legacyInjectRuleIdReferences = ({ return reference.name === 'alert_0'; }); if (referenceFound) { - if (referenceFound.id !== ruleAlertId) { - // This condition should not be reached but we log an error if we encounter it to help if we migrations - // did not run correctly or we create a regression in the future. - logger.error( - [ - 'The id of the "saved object reference id": ', - referenceFound.id, - ' is not the same as the "saved object id": ', - ruleAlertId, - '. Preferring and using the "saved object reference id" instead of the "saved object id"', - ].join('') - ); - } return referenceFound.id; } else { logger.error( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.test.ts index f0ff1b6072479..1212b73a6250e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.test.ts @@ -105,17 +105,6 @@ describe('inject_exceptions_list', () => { ).toEqual([{ ...mockExceptionsList()[0], id: '456' }]); }); - test('logs an error if found with a different saved object reference id', () => { - injectExceptionsReferences({ - logger, - exceptionsList: mockExceptionsList(), - savedObjectReferences: [{ ...mockSavedObjectReferences()[0], id: '456' }], - }); - expect(logger.error).toBeCalledWith( - 'The id of the "saved object reference id": 456 is not the same as the "saved object id": 123. Preferring and using the "saved object reference id" instead of the "saved object id"' - ); - }); - test('returns exceptionItem if the saved object reference cannot match as a fall back', () => { expect( injectExceptionsReferences({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.ts index 2e6559fbf18cf..baaaa2eb60ce9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.ts @@ -7,11 +7,7 @@ import { Logger, SavedObjectReference } from 'src/core/server'; import { RuleParams } from '../../schemas/rule_schemas'; -import { - getSavedObjectReferenceForExceptionsList, - logMissingSavedObjectError, - logWarningIfDifferentReferencesDetected, -} from './utils'; +import { getSavedObjectReferenceForExceptionsList, logMissingSavedObjectError } from './utils'; /** * This injects any "exceptionsList" "id"'s from saved object reference and returns the "exceptionsList" using the saved object reference. If for @@ -44,11 +40,6 @@ export const injectExceptionsReferences = ({ savedObjectReferences, }); if (savedObjectReference != null) { - logWarningIfDifferentReferencesDetected({ - logger, - savedObjectReferenceId: savedObjectReference.id, - savedObjectId: exceptionItem.id, - }); const reference: RuleParams['exceptionsList'][0] = { ...exceptionItem, id: savedObjectReference.id, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/index.ts index ca88dae364a4b..3a3d559a6ed39 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/index.ts @@ -11,4 +11,3 @@ export * from './get_saved_object_name_pattern'; export * from './get_saved_object_reference_for_exceptions_list'; export * from './get_saved_object_reference'; export * from './log_missing_saved_object_error'; -export * from './log_warning_if_different_references_detected'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.test.ts deleted file mode 100644 index a27faa6356c2b..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { loggingSystemMock } from 'src/core/server/mocks'; - -import { logWarningIfDifferentReferencesDetected } from '.'; - -describe('log_warning_if_different_references_detected', () => { - let logger = loggingSystemMock.create().get('security_solution'); - - beforeEach(() => { - logger = loggingSystemMock.create().get('security_solution'); - }); - - test('logs expect error message if the two ids are different', () => { - logWarningIfDifferentReferencesDetected({ - logger, - savedObjectReferenceId: '123', - savedObjectId: '456', - }); - expect(logger.error).toBeCalledWith( - 'The id of the "saved object reference id": 123 is not the same as the "saved object id": 456. Preferring and using the "saved object reference id" instead of the "saved object id"' - ); - }); - - test('logs nothing if the two ids are the same', () => { - logWarningIfDifferentReferencesDetected({ - logger, - savedObjectReferenceId: '123', - savedObjectId: '123', - }); - expect(logger.error).not.toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.ts deleted file mode 100644 index 9f80ba6d8ce83..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.ts +++ /dev/null @@ -1,36 +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 { Logger } from 'src/core/server'; - -/** - * This will log a warning that the saved object reference id and the saved object id are not the same if that is true. - * @param logger The kibana injected logger - * @param savedObjectReferenceId The saved object reference id from "references: [{ id: ...}]" - * @param savedObjectId The saved object id from a structure such as exceptions { exceptionsList: { "id": "..." } } - */ -export const logWarningIfDifferentReferencesDetected = ({ - logger, - savedObjectReferenceId, - savedObjectId, -}: { - logger: Logger; - savedObjectReferenceId: string; - savedObjectId: string; -}): void => { - if (savedObjectReferenceId !== savedObjectId) { - logger.error( - [ - 'The id of the "saved object reference id": ', - savedObjectReferenceId, - ' is not the same as the "saved object id": ', - savedObjectId, - '. Preferring and using the "saved object reference id" instead of the "saved object id"', - ].join('') - ); - } -}; From ee5b847533c1ff8d7418e815f6a425d2a2b363ad Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Thu, 21 Oct 2021 11:42:28 -0400 Subject: [PATCH 07/40] Fix label for fields in the Advanced settings section of policy details (#115869) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/management/pages/policy/view/policy_advanced.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx index 8e0d8c544563a..6034ed875c02b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx @@ -171,10 +171,10 @@ const PolicyAdvanced = React.memo( - {configPath.join('.')} + + {configPath.join('.')} {documentation && ( - + )} From 1b3f2cdedfa42fb3a5bb659becffae66c6b2c20b Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Thu, 21 Oct 2021 17:51:51 +0200 Subject: [PATCH 08/40] [Lens] Do not show legend actions popup when in non-interactive mode (#115804) * :bug: Fix non-interactive legend issue * :white_check_mark: Add tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../pie_visualization/render_function.test.tsx | 3 ++- .../pie_visualization/render_function.tsx | 2 +- .../xy_visualization/expression.test.tsx | 10 ++++++++++ .../public/xy_visualization/expression.tsx | 18 +++++++++++------- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx index 33e9154235147..ad4e30cd6e89f 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx @@ -302,12 +302,13 @@ describe('PieVisualization component', () => { `); }); - test('does not set click listener on non-interactive mode', () => { + test('does not set click listener and legend actions on non-interactive mode', () => { const defaultArgs = getDefaultArgs(); const component = shallow( ); expect(component.find(Settings).first().prop('onElementClick')).toBeUndefined(); + expect(component.find(Settings).first().prop('legendAction')).toBeUndefined(); }); test('it renders the empty placeholder when metric contains only falsy data', () => { diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index 834fecb95fc35..449b152523881 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -290,7 +290,7 @@ export function PieComponent( legendPosition={legendPosition || Position.Right} legendMaxDepth={nestedLegend ? undefined : 1 /* Color is based only on first layer */} onElementClick={props.interactive ?? true ? onElementClickHandler : undefined} - legendAction={getLegendAction(firstTable, onClickValue)} + legendAction={props.interactive ? getLegendAction(firstTable, onClickValue) : undefined} theme={{ ...chartTheme, background: { diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx index 48cddb0cd4e4d..5f9abba3806cf 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx @@ -1485,6 +1485,16 @@ describe('xy_expression', () => { expect(wrapper.find(Settings).first().prop('onElementClick')).toBeUndefined(); }); + test('legendAction is not triggering event on non-interactive mode', () => { + const { args, data } = sampleArgs(); + + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find(Settings).first().prop('legendAction')).toBeUndefined(); + }); + test('it renders stacked bar', () => { const { data, args } = sampleArgs(); const component = shallow( diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 915ea9d6cda94..32ca4c982c10e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -599,13 +599,17 @@ export function XYChart({ xDomain={xDomain} onBrushEnd={interactive ? (brushHandler as BrushEndListener) : undefined} onElementClick={interactive ? clickHandler : undefined} - legendAction={getLegendAction( - filteredLayers, - data.tables, - onClickValue, - formatFactory, - layersAlreadyFormatted - )} + legendAction={ + interactive + ? getLegendAction( + filteredLayers, + data.tables, + onClickValue, + formatFactory, + layersAlreadyFormatted + ) + : undefined + } showLegendExtra={isHistogramViz && valuesInLegend} /> From 6a80d9a53a20e10f3d9f08a330f54994ac31d9d7 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Thu, 21 Oct 2021 17:58:24 +0200 Subject: [PATCH 09/40] [Lens] Relax break down group validation for percentage charts (#114803) * :bug: Improved percentage checks for breakdown group * :white_check_mark: Add tests * :ok_hand: Integrate feedback Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../xy_visualization/visualization.test.ts | 208 ++++++++++++++++++ .../public/xy_visualization/visualization.tsx | 37 +++- 2 files changed, 244 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index 01fbbd892a118..973501816bc3e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -538,6 +538,214 @@ describe('xy_visualization', () => { expect(ops.filter(filterOperations).map((x) => x.dataType)).toEqual(['number']); }); + describe('breakdown group: percentage chart checks', () => { + const baseState = exampleState(); + + it('should require break down group with one accessor + one split accessor configuration', () => { + const [, , splitGroup] = xyVisualization.getConfiguration({ + state: { + ...baseState, + layers: [ + { ...baseState.layers[0], accessors: ['a'], seriesType: 'bar_percentage_stacked' }, + ], + }, + frame, + layerId: 'first', + }).groups; + expect(splitGroup.required).toBe(true); + }); + + test.each([ + [ + 'multiple accessors on the same layer', + [ + { + ...baseState.layers[0], + splitAccessor: undefined, + seriesType: 'bar_percentage_stacked', + }, + ], + ], + [ + 'multiple accessors spread on compatible layers', + [ + { + ...baseState.layers[0], + accessors: ['a'], + splitAccessor: undefined, + seriesType: 'bar_percentage_stacked', + }, + { + ...baseState.layers[0], + splitAccessor: undefined, + xAccessor: 'd', + accessors: ['e'], + seriesType: 'bar_percentage_stacked', + }, + ], + ], + ] as Array<[string, State['layers']]>)( + 'should not require break down group for %s', + (_, layers) => { + const [, , splitGroup] = xyVisualization.getConfiguration({ + state: { ...baseState, layers }, + frame, + layerId: 'first', + }).groups; + expect(splitGroup.required).toBe(false); + } + ); + + it.each([ + [ + 'one accessor only', + [ + { + ...baseState.layers[0], + accessors: ['a'], + seriesType: 'bar_percentage_stacked', + splitAccessor: undefined, + xAccessor: undefined, + }, + ], + ], + [ + 'one accessor only with split accessor', + [ + { + ...baseState.layers[0], + accessors: ['a'], + seriesType: 'bar_percentage_stacked', + xAccessor: undefined, + }, + ], + ], + [ + 'one accessor only with xAccessor', + [ + { + ...baseState.layers[0], + accessors: ['a'], + seriesType: 'bar_percentage_stacked', + splitAccessor: undefined, + }, + ], + ], + [ + 'multiple accessors spread on incompatible layers (different xAccessor)', + [ + { + ...baseState.layers[0], + accessors: ['a'], + seriesType: 'bar_percentage_stacked', + splitAccessor: undefined, + }, + { + ...baseState.layers[0], + accessors: ['e'], + seriesType: 'bar_percentage_stacked', + splitAccessor: undefined, + xAccessor: undefined, + }, + ], + ], + [ + 'multiple accessors spread on incompatible layers (different splitAccessor)', + [ + { + ...baseState.layers[0], + accessors: ['a'], + seriesType: 'bar_percentage_stacked', + }, + { + ...baseState.layers[0], + accessors: ['e'], + seriesType: 'bar_percentage_stacked', + splitAccessor: undefined, + xAccessor: undefined, + }, + ], + ], + [ + 'multiple accessors spread on incompatible layers (different seriesType)', + [ + { + ...baseState.layers[0], + accessors: ['a'], + seriesType: 'bar_percentage_stacked', + }, + { + ...baseState.layers[0], + accessors: ['e'], + seriesType: 'bar', + }, + ], + ], + [ + 'one data layer with one accessor + one reference layer', + [ + { + ...baseState.layers[0], + accessors: ['a'], + seriesType: 'bar_percentage_stacked', + }, + { + ...baseState.layers[0], + accessors: ['e'], + seriesType: 'bar_percentage_stacked', + layerType: layerTypes.REFERENCELINE, + }, + ], + ], + + [ + 'multiple accessors on the same layers with different axis assigned', + [ + { + ...baseState.layers[0], + splitAccessor: undefined, + seriesType: 'bar_percentage_stacked', + yConfig: [ + { forAccessor: 'a', axisMode: 'left' }, + { forAccessor: 'b', axisMode: 'right' }, + ], + }, + ], + ], + [ + 'multiple accessors spread on multiple layers with different axis assigned', + [ + { + ...baseState.layers[0], + accessors: ['a'], + xAccessor: undefined, + splitAccessor: undefined, + seriesType: 'bar_percentage_stacked', + yConfig: [{ forAccessor: 'a', axisMode: 'left' }], + }, + { + ...baseState.layers[0], + accessors: ['b'], + xAccessor: undefined, + splitAccessor: undefined, + seriesType: 'bar_percentage_stacked', + yConfig: [{ forAccessor: 'b', axisMode: 'right' }], + }, + ], + ], + ] as Array<[string, State['layers']]>)( + 'should require break down group for %s', + (_, layers) => { + const [, , splitGroup] = xyVisualization.getConfiguration({ + state: { ...baseState, layers }, + frame, + layerId: 'first', + }).groups; + expect(splitGroup.required).toBe(true); + } + ); + }); + describe('reference lines', () => { beforeEach(() => { frame.datasourceLayers = { diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index db1a2aeffb670..c23eccb196744 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -40,6 +40,7 @@ import { checkXAccessorCompatibility, getAxisName, } from './visualization_helpers'; +import { groupAxesByType } from './axes_configuration'; const defaultIcon = LensIconChartBarStacked; const defaultSeriesType = 'bar_stacked'; @@ -378,6 +379,40 @@ export const getXyVisualization = ({ }; } + const { left, right } = groupAxesByType([layer], frame.activeData); + // Check locally if it has one accessor OR one accessor per axis + const layerHasOnlyOneAccessor = Boolean( + layer.accessors.length < 2 || + (left.length && left.length < 2) || + (right.length && right.length < 2) + ); + // Check also for multiple layers that can stack for percentage charts + // Make sure that if multiple dimensions are defined for a single layer, they should belong to the same axis + const hasOnlyOneAccessor = + layerHasOnlyOneAccessor && + getLayersByType(state, layerTypes.DATA).filter( + // check that the other layers are compatible with this one + (dataLayer) => { + if ( + dataLayer.seriesType === layer.seriesType && + Boolean(dataLayer.xAccessor) === Boolean(layer.xAccessor) && + Boolean(dataLayer.splitAccessor) === Boolean(layer.splitAccessor) + ) { + const { left: localLeft, right: localRight } = groupAxesByType( + [dataLayer], + frame.activeData + ); + // return true only if matching axis are found + return ( + dataLayer.accessors.length && + (Boolean(localLeft.length) === Boolean(left.length) || + Boolean(localRight.length) === Boolean(right.length)) + ); + } + return false; + } + ).length < 2; + return { groups: [ { @@ -417,7 +452,7 @@ export const getXyVisualization = ({ filterOperations: isBucketed, supportsMoreColumns: !layer.splitAccessor, dataTestSubj: 'lnsXY_splitDimensionPanel', - required: layer.seriesType.includes('percentage'), + required: layer.seriesType.includes('percentage') && hasOnlyOneAccessor, enableDimensionEditor: true, }, ], From c491d8c3660acf16994e55b7c75663802a7a5a73 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 21 Oct 2021 09:00:01 -0700 Subject: [PATCH 10/40] Fixes python URL in documentation link service (#115886) --- src/core/public/doc_links/doc_links_service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 87b05eeafc568..20757463737fc 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -499,7 +499,7 @@ export class DocLinksService { netGuide: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/net-api/${DOC_LINK_VERSION}/index.html`, perlGuide: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/perl-api/${DOC_LINK_VERSION}/index.html`, phpGuide: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/php-api/${DOC_LINK_VERSION}/index.html`, - pythonGuide: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/net-api/${DOC_LINK_VERSION}/index.html`, + pythonGuide: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/python-api/${DOC_LINK_VERSION}/index.html`, rubyOverview: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/ruby-api/${DOC_LINK_VERSION}/ruby_client.html`, rustGuide: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/rust-api/${DOC_LINK_VERSION}/index.html`, }, From eb2b886a394670f6d5e9735fcaf4b5b9295cda79 Mon Sep 17 00:00:00 2001 From: Mark Hopkin Date: Thu, 21 Oct 2021 17:09:54 +0100 Subject: [PATCH 11/40] [Fleet] Link to the installed version of an integration from global search (#115736) * remove version from integrations global search link * fix tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../fleet/public/search_provider.test.ts | 24 ++++++++-------- .../plugins/fleet/public/search_provider.ts | 28 +++++++++---------- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/fleet/public/search_provider.test.ts b/x-pack/plugins/fleet/public/search_provider.test.ts index ef6bda44d512b..97ed199c44502 100644 --- a/x-pack/plugins/fleet/public/search_provider.test.ts +++ b/x-pack/plugins/fleet/public/search_provider.test.ts @@ -87,22 +87,22 @@ describe('Package search provider', () => { ).toBe('--(a|)', { a: [ { - id: 'test-test', + id: 'test', score: 80, title: 'test', type: 'integration', url: { - path: 'undefined/detail/test-test/overview', + path: 'undefined/detail/test/overview', prependBasePath: false, }, }, { - id: 'test1-test1', + id: 'test1', score: 80, title: 'test1', type: 'integration', url: { - path: 'undefined/detail/test1-test1/overview', + path: 'undefined/detail/test1/overview', prependBasePath: false, }, }, @@ -170,12 +170,12 @@ describe('Package search provider', () => { ).toBe('--(a|)', { a: [ { - id: 'test1-test1', + id: 'test1', score: 80, title: 'test1', type: 'integration', url: { - path: 'undefined/detail/test1-test1/overview', + path: 'undefined/detail/test1/overview', prependBasePath: false, }, }, @@ -226,22 +226,22 @@ describe('Package search provider', () => { ).toBe('--(a|)', { a: [ { - id: 'test-test', + id: 'test', score: 80, title: 'test', type: 'integration', url: { - path: 'undefined/detail/test-test/overview', + path: 'undefined/detail/test/overview', prependBasePath: false, }, }, { - id: 'test1-test1', + id: 'test1', score: 80, title: 'test1', type: 'integration', url: { - path: 'undefined/detail/test1-test1/overview', + path: 'undefined/detail/test1/overview', prependBasePath: false, }, }, @@ -269,12 +269,12 @@ describe('Package search provider', () => { ).toBe('--(a|)', { a: [ { - id: 'test1-test1', + id: 'test1', score: 80, title: 'test1', type: 'integration', url: { - path: 'undefined/detail/test1-test1/overview', + path: 'undefined/detail/test1/overview', prependBasePath: false, }, }, diff --git a/x-pack/plugins/fleet/public/search_provider.ts b/x-pack/plugins/fleet/public/search_provider.ts index 403abf89715c8..d919462f38c28 100644 --- a/x-pack/plugins/fleet/public/search_provider.ts +++ b/x-pack/plugins/fleet/public/search_provider.ts @@ -53,21 +53,19 @@ export const toSearchResult = ( pkg: PackageListItem, application: ApplicationStart, basePath: IBasePath -): GlobalSearchProviderResult => { - const pkgkey = `${pkg.name}-${pkg.version}`; - return { - id: pkgkey, - type: packageType, - title: pkg.title, - score: 80, - icon: getEuiIconType(pkg, basePath), - url: { - // prettier-ignore - path: `${application.getUrlForApp(INTEGRATIONS_PLUGIN_ID)}${pagePathGetters.integration_details_overview({ pkgkey })[1]}`, - prependBasePath: false, - }, - }; -}; +): GlobalSearchProviderResult => ({ + id: pkg.name, + type: packageType, + title: pkg.title, + score: 80, + icon: getEuiIconType(pkg, basePath), + url: { + path: `${application.getUrlForApp(INTEGRATIONS_PLUGIN_ID)}${ + pagePathGetters.integration_details_overview({ pkgkey: pkg.name })[1] + }`, + prependBasePath: false, + }, +}); export const createPackageSearchProvider = (core: CoreSetup): GlobalSearchResultProvider => { const coreStart$ = from(core.getStartServices()).pipe( From c12554b7a679cbce51b8673781e7e02489e61e08 Mon Sep 17 00:00:00 2001 From: Bhavya RM Date: Thu, 21 Oct 2021 12:20:34 -0400 Subject: [PATCH 12/40] Unskip dashboard and dashboard panel a11y tests (#115102) --- test/accessibility/apps/dashboard.ts | 3 +-- test/accessibility/apps/dashboard_panel.ts | 3 +-- test/accessibility/apps/discover.ts | 3 +-- test/functional/page_objects/home_page.ts | 6 +++++- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/test/accessibility/apps/dashboard.ts b/test/accessibility/apps/dashboard.ts index 408e7d402a8f0..54eb5e7df4178 100644 --- a/test/accessibility/apps/dashboard.ts +++ b/test/accessibility/apps/dashboard.ts @@ -15,8 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const listingTable = getService('listingTable'); - // FLAKY: https://github.com/elastic/kibana/issues/105171 - describe.skip('Dashboard', () => { + describe('Dashboard', () => { const dashboardName = 'Dashboard Listing A11y'; const clonedDashboardName = 'Dashboard Listing A11y Copy'; diff --git a/test/accessibility/apps/dashboard_panel.ts b/test/accessibility/apps/dashboard_panel.ts index b2fc073949d73..83c7776049d16 100644 --- a/test/accessibility/apps/dashboard_panel.ts +++ b/test/accessibility/apps/dashboard_panel.ts @@ -14,8 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const inspector = getService('inspector'); - // FLAKY: https://github.com/elastic/kibana/issues/112920 - describe.skip('Dashboard Panel', () => { + describe('Dashboard Panel', () => { before(async () => { await PageObjects.common.navigateToApp('dashboard'); await testSubjects.click('dashboardListingTitleLink-[Flights]-Global-Flight-Dashboard'); diff --git a/test/accessibility/apps/discover.ts b/test/accessibility/apps/discover.ts index e05f3e2bc091d..867e146e64ca3 100644 --- a/test/accessibility/apps/discover.ts +++ b/test/accessibility/apps/discover.ts @@ -92,8 +92,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.saveCurrentSavedQuery(); }); - // issue - https://github.com/elastic/kibana/issues/78488 - it.skip('a11y test on saved queries list panel', async () => { + it('a11y test on saved queries list panel', async () => { await PageObjects.discover.clickSavedQueriesPopOver(); await testSubjects.moveMouseTo( 'saved-query-list-item load-saved-query-test-button saved-query-list-item-selected saved-query-list-item-selected' diff --git a/test/functional/page_objects/home_page.ts b/test/functional/page_objects/home_page.ts index 29fdd1453b0e0..11b304cdbbf9d 100644 --- a/test/functional/page_objects/home_page.ts +++ b/test/functional/page_objects/home_page.ts @@ -13,6 +13,7 @@ export class HomePageObject extends FtrService { private readonly retry = this.ctx.getService('retry'); private readonly find = this.ctx.getService('find'); private readonly common = this.ctx.getPageObject('common'); + private readonly log = this.ctx.getService('log'); async clickSynopsis(title: string) { await this.testSubjects.click(`homeSynopsisLink${title}`); @@ -27,7 +28,10 @@ export class HomePageObject extends FtrService { } async isSampleDataSetInstalled(id: string) { - return !(await this.testSubjects.exists(`addSampleDataSet${id}`)); + const sampleDataCard = await this.testSubjects.find(`sampleDataSetCard${id}`); + const sampleDataCardInnerHTML = await sampleDataCard.getAttribute('innerHTML'); + this.log.debug(sampleDataCardInnerHTML); + return sampleDataCardInnerHTML.includes('removeSampleDataSet'); } async isWelcomeInterstitialDisplayed() { From 9d81fff7e82ed73b6afc9033eb4d63eadf5c453f Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Thu, 21 Oct 2021 09:27:50 -0700 Subject: [PATCH 13/40] Use extract/inject in saved query service (#111213) * Use extract/inject in saved query service * Add route handler context * Allow query objects & update tests * Add migrations to saved query type * Review feedback * Review feedback * Separate create/update methods * Fix copy/paste bug * Update jest snapshot --- .../common/query/persistable_state.test.ts | 7 +- .../data/common/query/persistable_state.ts | 10 +- src/plugins/data/common/query/types.ts | 23 +- src/plugins/data/public/plugin.ts | 4 +- .../data/public/query/query_service.ts | 10 +- .../saved_query/saved_query_service.test.ts | 446 ++------------ .../query/saved_query/saved_query_service.ts | 166 +---- .../data/public/query/saved_query/types.ts | 6 +- .../state_sync/connect_to_query_state.test.ts | 6 +- .../state_sync/sync_state_with_url.test.ts | 2 +- .../ui/saved_query_form/save_query_form.tsx | 25 +- .../data/public/ui/search_bar/search_bar.tsx | 11 +- .../data/server/query/query_service.ts | 12 +- .../query/route_handler_context.test.ts | 566 ++++++++++++++++++ .../server/query/route_handler_context.ts | 155 +++++ src/plugins/data/server/query/routes.ts | 144 +++++ .../server/saved_objects/migrations/query.ts | 42 ++ .../data/server/saved_objects/query.ts | 3 +- .../utils/saved_query_services/index.tsx | 8 +- .../components/timeline/query_bar/index.tsx | 34 +- 20 files changed, 1105 insertions(+), 575 deletions(-) create mode 100644 src/plugins/data/server/query/route_handler_context.test.ts create mode 100644 src/plugins/data/server/query/route_handler_context.ts create mode 100644 src/plugins/data/server/query/routes.ts create mode 100644 src/plugins/data/server/saved_objects/migrations/query.ts diff --git a/src/plugins/data/common/query/persistable_state.test.ts b/src/plugins/data/common/query/persistable_state.test.ts index 807cc72a071be..93f14a0fc2e08 100644 --- a/src/plugins/data/common/query/persistable_state.test.ts +++ b/src/plugins/data/common/query/persistable_state.test.ts @@ -8,6 +8,7 @@ import { extract, inject } from './persistable_state'; import { Filter } from '@kbn/es-query'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../common'; describe('filter manager persistable state tests', () => { const filters: Filter[] = [ @@ -15,13 +16,15 @@ describe('filter manager persistable state tests', () => { ]; describe('reference injection', () => { test('correctly inserts reference to filter', () => { - const updatedFilters = inject(filters, [{ type: 'index_pattern', name: 'test', id: '123' }]); + const updatedFilters = inject(filters, [ + { type: DATA_VIEW_SAVED_OBJECT_TYPE, name: 'test', id: '123' }, + ]); expect(updatedFilters[0]).toHaveProperty('meta.index', '123'); }); test('drops index setting if reference is missing', () => { const updatedFilters = inject(filters, [ - { type: 'index_pattern', name: 'test123', id: '123' }, + { type: DATA_VIEW_SAVED_OBJECT_TYPE, name: 'test123', id: '123' }, ]); expect(updatedFilters[0]).toHaveProperty('meta.index', undefined); }); diff --git a/src/plugins/data/common/query/persistable_state.ts b/src/plugins/data/common/query/persistable_state.ts index 934d481685db4..177aae391c4fb 100644 --- a/src/plugins/data/common/query/persistable_state.ts +++ b/src/plugins/data/common/query/persistable_state.ts @@ -8,7 +8,9 @@ import uuid from 'uuid'; import { Filter } from '@kbn/es-query'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../common'; import { SavedObjectReference } from '../../../../core/types'; +import { MigrateFunctionsObject } from '../../../kibana_utils/common'; export const extract = (filters: Filter[]) => { const references: SavedObjectReference[] = []; @@ -16,7 +18,7 @@ export const extract = (filters: Filter[]) => { if (filter.meta?.index) { const id = uuid(); references.push({ - type: 'index_pattern', + type: DATA_VIEW_SAVED_OBJECT_TYPE, name: id, id: filter.meta.index, }); @@ -54,6 +56,10 @@ export const telemetry = (filters: Filter[], collector: unknown) => { return {}; }; -export const getAllMigrations = () => { +export const migrateToLatest = (filters: Filter[], version: string) => { + return filters; +}; + +export const getAllMigrations = (): MigrateFunctionsObject => { return {}; }; diff --git a/src/plugins/data/common/query/types.ts b/src/plugins/data/common/query/types.ts index c1861beb1ed90..fea59ea558a35 100644 --- a/src/plugins/data/common/query/types.ts +++ b/src/plugins/data/common/query/types.ts @@ -6,6 +6,25 @@ * Side Public License, v 1. */ -export * from './timefilter/types'; +import type { Query, Filter } from '@kbn/es-query'; +import type { RefreshInterval, TimeRange } from './timefilter/types'; -export { Query } from '@kbn/es-query'; +export type { RefreshInterval, TimeRange, TimeRangeBounds } from './timefilter/types'; +export type { Query } from '@kbn/es-query'; + +export type SavedQueryTimeFilter = TimeRange & { + refreshInterval: RefreshInterval; +}; + +export interface SavedQuery { + id: string; + attributes: SavedQueryAttributes; +} + +export interface SavedQueryAttributes { + title: string; + description: string; + query: Query; + filters?: Filter[]; + timefilter?: SavedQueryTimeFilter; +} diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 4a55cc2a0d511..25f649f69a052 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -130,7 +130,7 @@ export class DataPublicPlugin core: CoreStart, { uiActions, fieldFormats, dataViews }: DataStartDependencies ): DataPublicPluginStart { - const { uiSettings, notifications, savedObjects, overlays } = core; + const { uiSettings, notifications, overlays } = core; setNotifications(notifications); setOverlays(overlays); setUiSettings(uiSettings); @@ -138,7 +138,7 @@ export class DataPublicPlugin const query = this.queryService.start({ storage: this.storage, - savedObjectsClient: savedObjects.client, + http: core.http, uiSettings, }); diff --git a/src/plugins/data/public/query/query_service.ts b/src/plugins/data/public/query/query_service.ts index 5104a934fdec8..314f13e3524db 100644 --- a/src/plugins/data/public/query/query_service.ts +++ b/src/plugins/data/public/query/query_service.ts @@ -7,7 +7,7 @@ */ import { share } from 'rxjs/operators'; -import { IUiSettingsClient, SavedObjectsClientContract } from 'src/core/public'; +import { HttpStart, IUiSettingsClient } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { buildEsQuery } from '@kbn/es-query'; import { FilterManager } from './filter_manager'; @@ -15,7 +15,7 @@ import { createAddToQueryLog } from './lib'; import { TimefilterService, TimefilterSetup } from './timefilter'; import { createSavedQueryService } from './saved_query/saved_query_service'; import { createQueryStateObservable } from './state_sync/create_global_query_observable'; -import { QueryStringManager, QueryStringContract } from './query_string'; +import { QueryStringContract, QueryStringManager } from './query_string'; import { getEsQueryConfig, TimeRange } from '../../common'; import { getUiSettings } from '../services'; import { NowProviderInternalContract } from '../now_provider'; @@ -33,9 +33,9 @@ interface QueryServiceSetupDependencies { } interface QueryServiceStartDependencies { - savedObjectsClient: SavedObjectsClientContract; storage: IStorageWrapper; uiSettings: IUiSettingsClient; + http: HttpStart; } export class QueryService { @@ -70,7 +70,7 @@ export class QueryService { }; } - public start({ savedObjectsClient, storage, uiSettings }: QueryServiceStartDependencies) { + public start({ storage, uiSettings, http }: QueryServiceStartDependencies) { return { addToQueryLog: createAddToQueryLog({ storage, @@ -78,7 +78,7 @@ export class QueryService { }), filterManager: this.filterManager, queryString: this.queryStringManager, - savedQueries: createSavedQueryService(savedObjectsClient), + savedQueries: createSavedQueryService(http), state$: this.state$, timefilter: this.timefilter, getEsQuery: (indexPattern: IndexPattern, timeRange?: TimeRange) => { diff --git a/src/plugins/data/public/query/saved_query/saved_query_service.test.ts b/src/plugins/data/public/query/saved_query/saved_query_service.test.ts index 673a86df98881..047051c302083 100644 --- a/src/plugins/data/public/query/saved_query/saved_query_service.test.ts +++ b/src/plugins/data/public/query/saved_query/saved_query_service.test.ts @@ -7,8 +7,20 @@ */ import { createSavedQueryService } from './saved_query_service'; -import { FilterStateStore } from '../../../common'; -import { SavedQueryAttributes } from './types'; +import { httpServiceMock } from '../../../../../core/public/mocks'; +import { SavedQueryAttributes } from '../../../common'; + +const http = httpServiceMock.createStartContract(); + +const { + deleteSavedQuery, + getSavedQuery, + findSavedQueries, + createQuery, + updateQuery, + getAllSavedQueries, + getSavedQueryCount, +} = createSavedQueryService(http); const savedQueryAttributes: SavedQueryAttributes = { title: 'foo', @@ -17,416 +29,90 @@ const savedQueryAttributes: SavedQueryAttributes = { language: 'kuery', query: 'response:200', }, -}; -const savedQueryAttributesBar: SavedQueryAttributes = { - title: 'bar', - description: 'baz', - query: { - language: 'kuery', - query: 'response:200', - }, -}; - -const savedQueryAttributesWithFilters: SavedQueryAttributes = { - ...savedQueryAttributes, - filters: [ - { - query: { match_all: {} }, - $state: { store: FilterStateStore.APP_STATE }, - meta: { - disabled: false, - negate: false, - alias: null, - }, - }, - ], - timefilter: { - to: 'now', - from: 'now-15m', - refreshInterval: { - pause: false, - value: 0, - }, - }, + filters: [], }; -const mockSavedObjectsClient = { - create: jest.fn(), - error: jest.fn(), - find: jest.fn(), - resolve: jest.fn(), - delete: jest.fn(), -}; - -const { - deleteSavedQuery, - getSavedQuery, - findSavedQueries, - saveQuery, - getAllSavedQueries, - getSavedQueryCount, -} = createSavedQueryService( - // @ts-ignore - mockSavedObjectsClient -); - describe('saved query service', () => { afterEach(() => { - mockSavedObjectsClient.create.mockReset(); - mockSavedObjectsClient.find.mockReset(); - mockSavedObjectsClient.resolve.mockReset(); - mockSavedObjectsClient.delete.mockReset(); + http.post.mockReset(); + http.get.mockReset(); + http.delete.mockReset(); }); - describe('saveQuery', function () { - it('should create a saved object for the given attributes', async () => { - mockSavedObjectsClient.create.mockReturnValue({ - id: 'foo', - attributes: savedQueryAttributes, + describe('createQuery', function () { + it('should post the stringified given attributes', async () => { + await createQuery(savedQueryAttributes); + expect(http.post).toBeCalled(); + expect(http.post).toHaveBeenCalledWith('/api/saved_query/_create', { + body: '{"title":"foo","description":"bar","query":{"language":"kuery","query":"response:200"},"filters":[]}', }); - - const response = await saveQuery(savedQueryAttributes); - expect(mockSavedObjectsClient.create).toHaveBeenCalledWith('query', savedQueryAttributes, { - id: 'foo', - }); - expect(response).toEqual({ id: 'foo', attributes: savedQueryAttributes }); }); + }); - it('should allow overwriting an existing saved query', async () => { - mockSavedObjectsClient.create.mockReturnValue({ - id: 'foo', - attributes: savedQueryAttributes, - }); - - const response = await saveQuery(savedQueryAttributes, { overwrite: true }); - expect(mockSavedObjectsClient.create).toHaveBeenCalledWith('query', savedQueryAttributes, { - id: 'foo', - overwrite: true, + describe('updateQuery', function () { + it('should put the ID & stringified given attributes', async () => { + await updateQuery('foo', savedQueryAttributes); + expect(http.put).toBeCalled(); + expect(http.put).toHaveBeenCalledWith('/api/saved_query/foo', { + body: '{"title":"foo","description":"bar","query":{"language":"kuery","query":"response:200"},"filters":[]}', }); - expect(response).toEqual({ id: 'foo', attributes: savedQueryAttributes }); }); + }); - it('should optionally accept filters and timefilters in object format', async () => { - const serializedSavedQueryAttributesWithFilters = { - ...savedQueryAttributesWithFilters, - filters: savedQueryAttributesWithFilters.filters, - timefilter: savedQueryAttributesWithFilters.timefilter, - }; - - mockSavedObjectsClient.create.mockReturnValue({ - id: 'foo', - attributes: serializedSavedQueryAttributesWithFilters, + describe('getAllSavedQueries', function () { + it('should post and extract the saved queries from the response', async () => { + http.post.mockResolvedValue({ + total: 0, + savedQueries: [{ attributes: savedQueryAttributes }], }); - - const response = await saveQuery(savedQueryAttributesWithFilters); - - expect(mockSavedObjectsClient.create).toHaveBeenCalledWith( - 'query', - serializedSavedQueryAttributesWithFilters, - { id: 'foo' } - ); - expect(response).toEqual({ id: 'foo', attributes: savedQueryAttributesWithFilters }); - }); - - it('should throw an error when saved objects client returns error', async () => { - mockSavedObjectsClient.create.mockReturnValue({ - error: { - error: '123', - message: 'An Error', - }, + const result = await getAllSavedQueries(); + expect(http.post).toBeCalled(); + expect(http.post).toHaveBeenCalledWith('/api/saved_query/_find', { + body: '{"perPage":10000}', }); - - let error = null; - try { - await saveQuery(savedQueryAttributes); - } catch (e) { - error = e; - } - expect(error).not.toBe(null); - }); - it('should throw an error if the saved query does not have a title', async () => { - let error = null; - try { - await saveQuery({ ...savedQueryAttributes, title: '' }); - } catch (e) { - error = e; - } - expect(error).not.toBe(null); + expect(result).toEqual([{ attributes: savedQueryAttributes }]); }); }); - describe('findSavedQueries', function () { - it('should find and return saved queries without search text or pagination parameters', async () => { - mockSavedObjectsClient.find.mockReturnValue({ - savedObjects: [{ id: 'foo', attributes: savedQueryAttributes }], - total: 5, - }); - - const response = await findSavedQueries(); - expect(response.queries).toEqual([{ id: 'foo', attributes: savedQueryAttributes }]); - }); - it('should return the total count along with the requested queries', async () => { - mockSavedObjectsClient.find.mockReturnValue({ - savedObjects: [{ id: 'foo', attributes: savedQueryAttributes }], - total: 5, - }); - - const response = await findSavedQueries(); - expect(response.total).toEqual(5); - }); - - it('should find and return saved queries with search text matching the title field', async () => { - mockSavedObjectsClient.find.mockReturnValue({ - savedObjects: [{ id: 'foo', attributes: savedQueryAttributes }], - total: 5, - }); - const response = await findSavedQueries('foo'); - expect(mockSavedObjectsClient.find).toHaveBeenCalledWith({ - page: 1, - perPage: 50, - search: 'foo', - searchFields: ['title^5', 'description'], - sortField: '_score', - type: 'query', - }); - expect(response.queries).toEqual([{ id: 'foo', attributes: savedQueryAttributes }]); - }); - it('should find and return parsed filters and timefilters items', async () => { - const serializedSavedQueryAttributesWithFilters = { - ...savedQueryAttributesWithFilters, - filters: savedQueryAttributesWithFilters.filters, - timefilter: savedQueryAttributesWithFilters.timefilter, - }; - mockSavedObjectsClient.find.mockReturnValue({ - savedObjects: [{ id: 'foo', attributes: serializedSavedQueryAttributesWithFilters }], - total: 5, - }); - const response = await findSavedQueries('bar'); - expect(response.queries).toEqual([ - { id: 'foo', attributes: savedQueryAttributesWithFilters }, - ]); - }); - it('should return an array of saved queries', async () => { - mockSavedObjectsClient.find.mockReturnValue({ - savedObjects: [{ id: 'foo', attributes: savedQueryAttributes }], - total: 5, + describe('findSavedQueries', function () { + it('should post and return the total & saved queries', async () => { + http.post.mockResolvedValue({ + total: 0, + savedQueries: [{ attributes: savedQueryAttributes }], }); - const response = await findSavedQueries(); - expect(response.queries).toEqual( - expect.objectContaining([ - { - attributes: { - description: 'bar', - query: { language: 'kuery', query: 'response:200' }, - title: 'foo', - }, - id: 'foo', - }, - ]) - ); - }); - it('should accept perPage and page properties', async () => { - mockSavedObjectsClient.find.mockReturnValue({ - savedObjects: [ - { id: 'foo', attributes: savedQueryAttributes }, - { id: 'bar', attributes: savedQueryAttributesBar }, - ], - total: 5, + const result = await findSavedQueries(); + expect(http.post).toBeCalled(); + expect(http.post).toHaveBeenCalledWith('/api/saved_query/_find', { + body: '{"page":1,"perPage":50,"search":""}', }); - const response = await findSavedQueries(undefined, 2, 1); - expect(mockSavedObjectsClient.find).toHaveBeenCalledWith({ - page: 1, - perPage: 2, - search: '', - searchFields: ['title^5', 'description'], - sortField: '_score', - type: 'query', + expect(result).toEqual({ + queries: [{ attributes: savedQueryAttributes }], + total: 0, }); - expect(response.queries).toEqual( - expect.objectContaining([ - { - attributes: { - description: 'bar', - query: { language: 'kuery', query: 'response:200' }, - title: 'foo', - }, - id: 'foo', - }, - { - attributes: { - description: 'baz', - query: { language: 'kuery', query: 'response:200' }, - title: 'bar', - }, - id: 'bar', - }, - ]) - ); }); }); describe('getSavedQuery', function () { - it('should retrieve a saved query by id', async () => { - mockSavedObjectsClient.resolve.mockReturnValue({ - saved_object: { - id: 'foo', - attributes: savedQueryAttributes, - }, - outcome: 'exactMatch', - }); - - const response = await getSavedQuery('foo'); - expect(response).toEqual({ id: 'foo', attributes: savedQueryAttributes }); - }); - it('should only return saved queries', async () => { - mockSavedObjectsClient.resolve.mockReturnValue({ - saved_object: { - id: 'foo', - attributes: savedQueryAttributes, - }, - outcome: 'exactMatch', - }); - - await getSavedQuery('foo'); - expect(mockSavedObjectsClient.resolve).toHaveBeenCalledWith('query', 'foo'); - }); - - it('should parse a json query', async () => { - mockSavedObjectsClient.resolve.mockReturnValue({ - saved_object: { - id: 'food', - attributes: { - title: 'food', - description: 'bar', - query: { - language: 'kuery', - query: '{"x": "y"}', - }, - }, - }, - outcome: 'exactMatch', - }); - - const response = await getSavedQuery('food'); - expect(response.attributes.query.query).toEqual({ x: 'y' }); - }); - - it('should handle null string', async () => { - mockSavedObjectsClient.resolve.mockReturnValue({ - saved_object: { - id: 'food', - attributes: { - title: 'food', - description: 'bar', - query: { - language: 'kuery', - query: 'null', - }, - }, - }, - outcome: 'exactMatch', - }); - - const response = await getSavedQuery('food'); - expect(response.attributes.query.query).toEqual('null'); - }); - - it('should handle null quoted string', async () => { - mockSavedObjectsClient.resolve.mockReturnValue({ - saved_object: { - id: 'food', - attributes: { - title: 'food', - description: 'bar', - query: { - language: 'kuery', - query: '"null"', - }, - }, - }, - outcome: 'exactMatch', - }); - - const response = await getSavedQuery('food'); - expect(response.attributes.query.query).toEqual('"null"'); - }); - - it('should not lose quotes', async () => { - mockSavedObjectsClient.resolve.mockReturnValue({ - saved_object: { - id: 'food', - attributes: { - title: 'food', - description: 'bar', - query: { - language: 'kuery', - query: '"Bob"', - }, - }, - }, - outcome: 'exactMatch', - }); - - const response = await getSavedQuery('food'); - expect(response.attributes.query.query).toEqual('"Bob"'); - }); - - it('should throw if conflict', async () => { - mockSavedObjectsClient.resolve.mockReturnValue({ - saved_object: { - id: 'foo', - attributes: savedQueryAttributes, - }, - outcome: 'conflict', - }); - - const result = getSavedQuery('food'); - expect(result).rejects.toMatchInlineSnapshot( - `[Error: Multiple saved queries found with ID: food (legacy URL alias conflict)]` - ); + it('should get the given ID', async () => { + await getSavedQuery('my_id'); + expect(http.get).toBeCalled(); + expect(http.get).toHaveBeenCalledWith('/api/saved_query/my_id'); }); }); describe('deleteSavedQuery', function () { - it('should delete the saved query for the given ID', async () => { - await deleteSavedQuery('foo'); - expect(mockSavedObjectsClient.delete).toHaveBeenCalledWith('query', 'foo'); - }); - }); - - describe('getAllSavedQueries', function () { - it('should return all the saved queries', async () => { - mockSavedObjectsClient.find.mockReturnValue({ - savedObjects: [{ id: 'foo', attributes: savedQueryAttributes }], - }); - const response = await getAllSavedQueries(); - expect(response).toEqual( - expect.objectContaining([ - { - attributes: { - description: 'bar', - query: { language: 'kuery', query: 'response:200' }, - title: 'foo', - }, - id: 'foo', - }, - ]) - ); - expect(mockSavedObjectsClient.find).toHaveBeenCalledWith({ - page: 1, - perPage: 0, - type: 'query', - }); + it('should delete the given ID', async () => { + await deleteSavedQuery('my_id'); + expect(http.delete).toBeCalled(); + expect(http.delete).toHaveBeenCalledWith('/api/saved_query/my_id'); }); }); describe('getSavedQueryCount', function () { - it('should return the total number of saved queries', async () => { - mockSavedObjectsClient.find.mockReturnValue({ - total: 1, - }); - const response = await getSavedQueryCount(); - expect(response).toEqual(1); + it('should get the total', async () => { + await getSavedQueryCount(); + expect(http.get).toBeCalled(); + expect(http.get).toHaveBeenCalledWith('/api/saved_query/_count'); }); }); }); diff --git a/src/plugins/data/public/query/saved_query/saved_query_service.ts b/src/plugins/data/public/query/saved_query/saved_query_service.ts index 89a357a66d370..8ec9167a3a0c2 100644 --- a/src/plugins/data/public/query/saved_query/saved_query_service.ts +++ b/src/plugins/data/public/query/saved_query/saved_query_service.ts @@ -6,163 +6,61 @@ * Side Public License, v 1. */ -import { isObject } from 'lodash'; -import { SavedObjectsClientContract, SavedObjectAttributes } from 'src/core/public'; -import { SavedQueryAttributes, SavedQuery, SavedQueryService } from './types'; - -type SerializedSavedQueryAttributes = SavedObjectAttributes & - SavedQueryAttributes & { - query: { - query: string; - language: string; - }; +import { HttpStart } from 'src/core/public'; +import { SavedQuery } from './types'; +import { SavedQueryAttributes } from '../../../common'; + +export const createSavedQueryService = (http: HttpStart) => { + const createQuery = async (attributes: SavedQueryAttributes, { overwrite = false } = {}) => { + const savedQuery = await http.post('/api/saved_query/_create', { + body: JSON.stringify(attributes), + }); + return savedQuery; }; -export const createSavedQueryService = ( - savedObjectsClient: SavedObjectsClientContract -): SavedQueryService => { - const saveQuery = async (attributes: SavedQueryAttributes, { overwrite = false } = {}) => { - if (!attributes.title.length) { - // title is required extra check against circumventing the front end - throw new Error('Cannot create saved query without a title'); - } - - const query = { - query: - typeof attributes.query.query === 'string' - ? attributes.query.query - : JSON.stringify(attributes.query.query), - language: attributes.query.language, - }; - - const queryObject: SerializedSavedQueryAttributes = { - title: attributes.title.trim(), // trim whitespace before save as an extra precaution against circumventing the front end - description: attributes.description, - query, - }; - - if (attributes.filters) { - queryObject.filters = attributes.filters; - } - - if (attributes.timefilter) { - queryObject.timefilter = attributes.timefilter; - } - - let rawQueryResponse; - if (!overwrite) { - rawQueryResponse = await savedObjectsClient.create('query', queryObject, { - id: attributes.title, - }); - } else { - rawQueryResponse = await savedObjectsClient.create('query', queryObject, { - id: attributes.title, - overwrite: true, - }); - } - - if (rawQueryResponse.error) { - throw new Error(rawQueryResponse.error.message); - } - - return parseSavedQueryObject(rawQueryResponse); + const updateQuery = async (id: string, attributes: SavedQueryAttributes) => { + const savedQuery = await http.put(`/api/saved_query/${id}`, { + body: JSON.stringify(attributes), + }); + return savedQuery; }; + // we have to tell the saved objects client how many to fetch, otherwise it defaults to fetching 20 per page const getAllSavedQueries = async (): Promise => { - const count = await getSavedQueryCount(); - const response = await savedObjectsClient.find({ - type: 'query', - perPage: count, - page: 1, + const { savedQueries } = await http.post('/api/saved_query/_find', { + body: JSON.stringify({ perPage: 10000 }), }); - return response.savedObjects.map( - (savedObject: { id: string; attributes: SerializedSavedQueryAttributes }) => - parseSavedQueryObject(savedObject) - ); + return savedQueries; }; + // findSavedQueries will do a 'match_all' if no search string is passed in const findSavedQueries = async ( - searchText: string = '', + search: string = '', perPage: number = 50, - activePage: number = 1 + page: number = 1 ): Promise<{ total: number; queries: SavedQuery[] }> => { - const response = await savedObjectsClient.find({ - type: 'query', - search: searchText, - searchFields: ['title^5', 'description'], - sortField: '_score', - perPage, - page: activePage, + const { total, savedQueries: queries } = await http.post('/api/saved_query/_find', { + body: JSON.stringify({ page, perPage, search }), }); - return { - total: response.total, - queries: response.savedObjects.map( - (savedObject: { id: string; attributes: SerializedSavedQueryAttributes }) => - parseSavedQueryObject(savedObject) - ), - }; - }; - - const getSavedQuery = async (id: string): Promise => { - const { saved_object: savedObject, outcome } = - await savedObjectsClient.resolve('query', id); - if (outcome === 'conflict') { - throw new Error(`Multiple saved queries found with ID: ${id} (legacy URL alias conflict)`); - } else if (savedObject.error) { - throw new Error(savedObject.error.message); - } - return parseSavedQueryObject(savedObject); + return { total, queries }; }; - const deleteSavedQuery = async (id: string) => { - return await savedObjectsClient.delete('query', id); + const getSavedQuery = (id: string): Promise => { + return http.get(`/api/saved_query/${id}`); }; - const parseSavedQueryObject = (savedQuery: { - id: string; - attributes: SerializedSavedQueryAttributes; - }) => { - let queryString: string | object = savedQuery.attributes.query.query; - - try { - const parsedQueryString: object = JSON.parse(savedQuery.attributes.query.query); - if (isObject(parsedQueryString)) { - queryString = parsedQueryString; - } - } catch (e) {} // eslint-disable-line no-empty - - const savedQueryItems: SavedQueryAttributes = { - title: savedQuery.attributes.title || '', - description: savedQuery.attributes.description || '', - query: { - query: queryString, - language: savedQuery.attributes.query.language, - }, - }; - if (savedQuery.attributes.filters) { - savedQueryItems.filters = savedQuery.attributes.filters; - } - if (savedQuery.attributes.timefilter) { - savedQueryItems.timefilter = savedQuery.attributes.timefilter; - } - return { - id: savedQuery.id, - attributes: savedQueryItems, - }; + const deleteSavedQuery = (id: string) => { + return http.delete(`/api/saved_query/${id}`); }; const getSavedQueryCount = async (): Promise => { - const response = await savedObjectsClient.find({ - type: 'query', - perPage: 0, - page: 1, - }); - return response.total; + return http.get('/api/saved_query/_count'); }; return { - saveQuery, + createQuery, + updateQuery, getAllSavedQueries, findSavedQueries, getSavedQuery, diff --git a/src/plugins/data/public/query/saved_query/types.ts b/src/plugins/data/public/query/saved_query/types.ts index bd53bb7d77b30..0f1763433e72a 100644 --- a/src/plugins/data/public/query/saved_query/types.ts +++ b/src/plugins/data/public/query/saved_query/types.ts @@ -26,10 +26,8 @@ export interface SavedQueryAttributes { } export interface SavedQueryService { - saveQuery: ( - attributes: SavedQueryAttributes, - config?: { overwrite: boolean } - ) => Promise; + createQuery: (attributes: SavedQueryAttributes) => Promise; + updateQuery: (id: string, attributes: SavedQueryAttributes) => Promise; getAllSavedQueries: () => Promise; findSavedQueries: ( searchText?: string, diff --git a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts index b4ec4934233d0..857a932d9157b 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts @@ -74,7 +74,7 @@ describe('connect_to_global_state', () => { queryServiceStart = queryService.start({ uiSettings: setupMock.uiSettings, storage: new Storage(new StubBrowserStorage()), - savedObjectsClient: startMock.savedObjects.client, + http: startMock.http, }); filterManager = queryServiceStart.filterManager; timeFilter = queryServiceStart.timefilter.timefilter; @@ -308,7 +308,7 @@ describe('connect_to_app_state', () => { queryServiceStart = queryService.start({ uiSettings: setupMock.uiSettings, storage: new Storage(new StubBrowserStorage()), - savedObjectsClient: startMock.savedObjects.client, + http: startMock.http, }); filterManager = queryServiceStart.filterManager; @@ -487,7 +487,7 @@ describe('filters with different state', () => { queryServiceStart = queryService.start({ uiSettings: setupMock.uiSettings, storage: new Storage(new StubBrowserStorage()), - savedObjectsClient: startMock.savedObjects.client, + http: startMock.http, }); filterManager = queryServiceStart.filterManager; diff --git a/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts b/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts index 73f78eb98968d..2e48a11efd69c 100644 --- a/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts +++ b/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts @@ -68,7 +68,7 @@ describe('sync_query_state_with_url', () => { queryServiceStart = queryService.start({ uiSettings: startMock.uiSettings, storage: new Storage(new StubBrowserStorage()), - savedObjectsClient: startMock.savedObjects.client, + http: startMock.http, }); filterManager = queryServiceStart.filterManager; timefilter = queryServiceStart.timefilter.timefilter; diff --git a/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx b/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx index d0221658f3e08..c7a79658fac88 100644 --- a/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx +++ b/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx @@ -24,10 +24,9 @@ import { import { i18n } from '@kbn/i18n'; import { sortBy, isEqual } from 'lodash'; import { SavedQuery, SavedQueryService } from '../..'; -import { SavedQueryAttributes } from '../../query'; interface Props { - savedQuery?: SavedQueryAttributes; + savedQuery?: SavedQuery; savedQueryService: SavedQueryService; onSave: (savedQueryMeta: SavedQueryMeta) => void; onClose: () => void; @@ -36,6 +35,7 @@ interface Props { } export interface SavedQueryMeta { + id?: string; title: string; description: string; shouldIncludeFilters: boolean; @@ -50,18 +50,18 @@ export function SaveQueryForm({ showFilterOption = true, showTimeFilterOption = true, }: Props) { - const [title, setTitle] = useState(savedQuery ? savedQuery.title : ''); + const [title, setTitle] = useState(savedQuery?.attributes.title ?? ''); const [enabledSaveButton, setEnabledSaveButton] = useState(Boolean(savedQuery)); - const [description, setDescription] = useState(savedQuery ? savedQuery.description : ''); + const [description, setDescription] = useState(savedQuery?.attributes.description ?? ''); const [savedQueries, setSavedQueries] = useState([]); const [shouldIncludeFilters, setShouldIncludeFilters] = useState( - savedQuery ? !!savedQuery.filters : true + Boolean(savedQuery?.attributes.filters ?? true) ); // Defaults to false because saved queries are meant to be as portable as possible and loading // a saved query with a time filter will override whatever the current value of the global timepicker // is. We expect this option to be used rarely and only when the user knows they want this behavior. const [shouldIncludeTimefilter, setIncludeTimefilter] = useState( - savedQuery ? !!savedQuery.timefilter : false + Boolean(savedQuery?.attributes.timefilter ?? false) ); const [formErrors, setFormErrors] = useState([]); @@ -82,7 +82,7 @@ export function SaveQueryForm({ useEffect(() => { const fetchQueries = async () => { const allSavedQueries = await savedQueryService.getAllSavedQueries(); - const sortedAllSavedQueries = sortBy(allSavedQueries, 'attributes.title') as SavedQuery[]; + const sortedAllSavedQueries = sortBy(allSavedQueries, 'attributes.title'); setSavedQueries(sortedAllSavedQueries); }; fetchQueries(); @@ -109,13 +109,22 @@ export function SaveQueryForm({ const onClickSave = useCallback(() => { if (validate()) { onSave({ + id: savedQuery?.id, title, description, shouldIncludeFilters, shouldIncludeTimefilter, }); } - }, [validate, onSave, title, description, shouldIncludeFilters, shouldIncludeTimefilter]); + }, [ + validate, + onSave, + savedQuery?.id, + title, + description, + shouldIncludeFilters, + shouldIncludeTimefilter, + ]); const onInputChange = useCallback((event) => { setEnabledSaveButton(Boolean(event.target.value)); diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index db0bebf97578b..bd48dcd6cd34c 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -245,11 +245,12 @@ class SearchBarUI extends Component { try { let response; if (this.props.savedQuery && !saveAsNew) { - response = await this.savedQueryService.saveQuery(savedQueryAttributes, { - overwrite: true, - }); + response = await this.savedQueryService.updateQuery( + savedQueryMeta.id!, + savedQueryAttributes + ); } else { - response = await this.savedQueryService.saveQuery(savedQueryAttributes); + response = await this.savedQueryService.createQuery(savedQueryAttributes); } this.services.notifications.toasts.addSuccess( @@ -423,7 +424,7 @@ class SearchBarUI extends Component { {this.state.showSaveQueryModal ? ( this.setState({ showSaveQueryModal: false })} diff --git a/src/plugins/data/server/query/query_service.ts b/src/plugins/data/server/query/query_service.ts index 1bf5ff901e90f..173abeda0c951 100644 --- a/src/plugins/data/server/query/query_service.ts +++ b/src/plugins/data/server/query/query_service.ts @@ -8,11 +8,21 @@ import { CoreSetup, Plugin } from 'kibana/server'; import { querySavedObjectType } from '../saved_objects'; -import { extract, inject, telemetry, getAllMigrations } from '../../common/query/persistable_state'; +import { extract, getAllMigrations, inject, telemetry } from '../../common/query/persistable_state'; +import { registerSavedQueryRoutes } from './routes'; +import { + registerSavedQueryRouteHandlerContext, + SavedQueryRouteHandlerContext, +} from './route_handler_context'; export class QueryService implements Plugin { public setup(core: CoreSetup) { core.savedObjects.registerType(querySavedObjectType); + core.http.registerRouteHandlerContext( + 'savedQuery', + registerSavedQueryRouteHandlerContext + ); + registerSavedQueryRoutes(core); return { filterManager: { diff --git a/src/plugins/data/server/query/route_handler_context.test.ts b/src/plugins/data/server/query/route_handler_context.test.ts new file mode 100644 index 0000000000000..cc7686a06cb67 --- /dev/null +++ b/src/plugins/data/server/query/route_handler_context.test.ts @@ -0,0 +1,566 @@ +/* + * 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 { coreMock } from '../../../../core/server/mocks'; +import { + DATA_VIEW_SAVED_OBJECT_TYPE, + FilterStateStore, + SavedObject, + SavedQueryAttributes, +} from '../../common'; +import { registerSavedQueryRouteHandlerContext } from './route_handler_context'; +import { SavedObjectsFindResponse, SavedObjectsUpdateResponse } from 'kibana/server'; + +const mockContext = { + core: coreMock.createRequestHandlerContext(), +}; +const { + core: { + savedObjects: { client: mockSavedObjectsClient }, + }, +} = mockContext; +const context = registerSavedQueryRouteHandlerContext(mockContext); + +const savedQueryAttributes: SavedQueryAttributes = { + title: 'foo', + description: 'bar', + query: { + language: 'kuery', + query: 'response:200', + }, + filters: [], +}; +const savedQueryAttributesBar: SavedQueryAttributes = { + title: 'bar', + description: 'baz', + query: { + language: 'kuery', + query: 'response:200', + }, +}; + +const savedQueryAttributesWithFilters: SavedQueryAttributes = { + ...savedQueryAttributes, + filters: [ + { + query: { match_all: {} }, + $state: { store: FilterStateStore.APP_STATE }, + meta: { + index: 'my-index', + disabled: false, + negate: false, + alias: null, + }, + }, + ], + timefilter: { + to: 'now', + from: 'now-15m', + refreshInterval: { + pause: false, + value: 0, + }, + }, +}; + +const savedQueryReferences = [ + { + type: DATA_VIEW_SAVED_OBJECT_TYPE, + name: 'my-index', + id: 'my-index', + }, +]; + +describe('saved query route handler context', () => { + beforeEach(() => { + mockSavedObjectsClient.create.mockClear(); + mockSavedObjectsClient.resolve.mockClear(); + mockSavedObjectsClient.find.mockClear(); + mockSavedObjectsClient.delete.mockClear(); + }); + + describe('create', function () { + it('should create a saved object for the given attributes', async () => { + const mockResponse: SavedObject = { + id: 'foo', + type: 'query', + attributes: savedQueryAttributes, + references: [], + }; + mockSavedObjectsClient.create.mockResolvedValue(mockResponse); + + const response = await context.create(savedQueryAttributes); + + expect(mockSavedObjectsClient.create).toHaveBeenCalledWith('query', savedQueryAttributes, { + references: [], + }); + expect(response).toEqual({ + id: 'foo', + attributes: savedQueryAttributes, + }); + }); + + it('should optionally accept query in object format', async () => { + const savedQueryAttributesWithQueryObject: SavedQueryAttributes = { + ...savedQueryAttributes, + query: { + language: 'lucene', + query: { match_all: {} }, + }, + }; + const mockResponse: SavedObject = { + id: 'foo', + type: 'query', + attributes: savedQueryAttributesWithQueryObject, + references: [], + }; + mockSavedObjectsClient.create.mockResolvedValue(mockResponse); + + const { attributes } = await context.create(savedQueryAttributesWithQueryObject); + + expect(attributes).toEqual(savedQueryAttributesWithQueryObject); + }); + + it('should optionally accept filters and timefilters in object format', async () => { + const serializedSavedQueryAttributesWithFilters = { + ...savedQueryAttributesWithFilters, + filters: savedQueryAttributesWithFilters.filters, + timefilter: savedQueryAttributesWithFilters.timefilter, + }; + const mockResponse: SavedObject = { + id: 'foo', + type: 'query', + attributes: serializedSavedQueryAttributesWithFilters, + references: [], + }; + mockSavedObjectsClient.create.mockResolvedValue(mockResponse); + + await context.create(savedQueryAttributesWithFilters); + + const [[type, attributes]] = mockSavedObjectsClient.create.mock.calls; + const { filters = [], timefilter } = attributes as SavedQueryAttributes; + expect(type).toEqual('query'); + expect(filters.length).toBe(1); + expect(timefilter).toEqual(savedQueryAttributesWithFilters.timefilter); + }); + + it('should throw an error when saved objects client returns error', async () => { + mockSavedObjectsClient.create.mockResolvedValue({ + error: { + error: '123', + message: 'An Error', + }, + } as SavedObject); + + const response = context.create(savedQueryAttributes); + + expect(response).rejects.toMatchInlineSnapshot(`[Error: An Error]`); + }); + + it('should throw an error if the saved query does not have a title', async () => { + const response = context.create({ ...savedQueryAttributes, title: '' }); + expect(response).rejects.toMatchInlineSnapshot( + `[Error: Cannot create saved query without a title]` + ); + }); + }); + + describe('update', function () { + it('should update a saved object for the given attributes', async () => { + const mockResponse: SavedObject = { + id: 'foo', + type: 'query', + attributes: savedQueryAttributes, + references: [], + }; + mockSavedObjectsClient.update.mockResolvedValue(mockResponse); + + const response = await context.update('foo', savedQueryAttributes); + + expect(mockSavedObjectsClient.update).toHaveBeenCalledWith( + 'query', + 'foo', + savedQueryAttributes, + { + references: [], + } + ); + expect(response).toEqual({ + id: 'foo', + attributes: savedQueryAttributes, + }); + }); + + it('should throw an error when saved objects client returns error', async () => { + mockSavedObjectsClient.update.mockResolvedValue({ + error: { + error: '123', + message: 'An Error', + }, + } as SavedObjectsUpdateResponse); + + const response = context.update('foo', savedQueryAttributes); + + expect(response).rejects.toMatchInlineSnapshot(`[Error: An Error]`); + }); + + it('should throw an error if the saved query does not have a title', async () => { + const response = context.create({ ...savedQueryAttributes, title: '' }); + expect(response).rejects.toMatchInlineSnapshot( + `[Error: Cannot create saved query without a title]` + ); + }); + }); + + describe('find', function () { + it('should find and return saved queries without search text or pagination parameters', async () => { + const mockResponse: SavedObjectsFindResponse = { + page: 0, + per_page: 0, + saved_objects: [ + { + id: 'foo', + type: 'query', + score: 0, + attributes: savedQueryAttributes, + references: [], + }, + ], + total: 5, + }; + mockSavedObjectsClient.find.mockResolvedValue(mockResponse); + + const response = await context.find(); + + expect(response.savedQueries).toEqual([{ id: 'foo', attributes: savedQueryAttributes }]); + }); + + it('should return the total count along with the requested queries', async () => { + const mockResponse: SavedObjectsFindResponse = { + page: 0, + per_page: 0, + saved_objects: [ + { id: 'foo', type: 'query', score: 0, attributes: savedQueryAttributes, references: [] }, + ], + total: 5, + }; + mockSavedObjectsClient.find.mockResolvedValue(mockResponse); + + const response = await context.find(); + + expect(response.total).toEqual(5); + }); + + it('should find and return saved queries with search text matching the title field', async () => { + const mockResponse: SavedObjectsFindResponse = { + page: 0, + per_page: 0, + saved_objects: [ + { id: 'foo', type: 'query', score: 0, attributes: savedQueryAttributes, references: [] }, + ], + total: 5, + }; + mockSavedObjectsClient.find.mockResolvedValue(mockResponse); + + const response = await context.find({ search: 'foo' }); + + expect(mockSavedObjectsClient.find).toHaveBeenCalledWith({ + page: 1, + perPage: 50, + search: 'foo', + type: 'query', + }); + expect(response.savedQueries).toEqual([{ id: 'foo', attributes: savedQueryAttributes }]); + }); + + it('should find and return parsed filters and timefilters items', async () => { + const mockResponse: SavedObjectsFindResponse = { + page: 0, + per_page: 0, + saved_objects: [ + { + id: 'foo', + type: 'query', + score: 0, + attributes: savedQueryAttributesWithFilters, + references: savedQueryReferences, + }, + ], + total: 5, + }; + mockSavedObjectsClient.find.mockResolvedValue(mockResponse); + + const response = await context.find({ search: 'bar' }); + + expect(response.savedQueries).toEqual([ + { id: 'foo', attributes: savedQueryAttributesWithFilters }, + ]); + }); + + it('should return an array of saved queries', async () => { + const mockResponse: SavedObjectsFindResponse = { + page: 0, + per_page: 0, + saved_objects: [ + { id: 'foo', type: 'query', score: 0, attributes: savedQueryAttributes, references: [] }, + ], + total: 5, + }; + mockSavedObjectsClient.find.mockResolvedValue(mockResponse); + + const response = await context.find(); + + expect(response.savedQueries).toEqual( + expect.objectContaining([ + { + attributes: { + description: 'bar', + query: { language: 'kuery', query: 'response:200' }, + filters: [], + title: 'foo', + }, + id: 'foo', + }, + ]) + ); + }); + + it('should accept perPage and page properties', async () => { + const mockResponse: SavedObjectsFindResponse = { + page: 0, + per_page: 0, + saved_objects: [ + { id: 'foo', type: 'query', score: 0, attributes: savedQueryAttributes, references: [] }, + { + id: 'bar', + type: 'query', + score: 0, + attributes: savedQueryAttributesBar, + references: [], + }, + ], + total: 5, + }; + mockSavedObjectsClient.find.mockResolvedValue(mockResponse); + + const response = await context.find({ + page: 1, + perPage: 2, + }); + + expect(mockSavedObjectsClient.find).toHaveBeenCalledWith({ + page: 1, + perPage: 2, + search: '', + type: 'query', + }); + expect(response.savedQueries).toEqual( + expect.objectContaining([ + { + attributes: { + description: 'bar', + query: { language: 'kuery', query: 'response:200' }, + filters: [], + title: 'foo', + }, + id: 'foo', + }, + { + attributes: { + description: 'baz', + query: { language: 'kuery', query: 'response:200' }, + filters: [], + title: 'bar', + }, + id: 'bar', + }, + ]) + ); + }); + }); + + describe('get', function () { + it('should retrieve a saved query by id', async () => { + mockSavedObjectsClient.resolve.mockResolvedValue({ + saved_object: { + id: 'foo', + type: 'query', + attributes: savedQueryAttributes, + references: [], + }, + outcome: 'exactMatch', + }); + + const response = await context.get('foo'); + expect(response).toEqual({ id: 'foo', attributes: savedQueryAttributes }); + }); + + it('should only return saved queries', async () => { + mockSavedObjectsClient.resolve.mockResolvedValue({ + saved_object: { + id: 'foo', + type: 'query', + attributes: savedQueryAttributes, + references: [], + }, + outcome: 'exactMatch', + }); + + await context.get('foo'); + expect(mockSavedObjectsClient.resolve).toHaveBeenCalledWith('query', 'foo'); + }); + + it('should parse a json query', async () => { + mockSavedObjectsClient.resolve.mockResolvedValue({ + saved_object: { + id: 'food', + type: 'query', + attributes: { + title: 'food', + description: 'bar', + query: { + language: 'kuery', + query: '{"x": "y"}', + }, + }, + references: [], + }, + outcome: 'exactMatch', + }); + + const response = await context.get('food'); + expect(response.attributes.query.query).toEqual({ x: 'y' }); + }); + + it('should handle null string', async () => { + mockSavedObjectsClient.resolve.mockResolvedValue({ + saved_object: { + id: 'food', + type: 'query', + attributes: { + title: 'food', + description: 'bar', + query: { + language: 'kuery', + query: 'null', + }, + }, + references: [], + }, + outcome: 'exactMatch', + }); + + const response = await context.get('food'); + expect(response.attributes.query.query).toEqual('null'); + }); + + it('should handle null quoted string', async () => { + mockSavedObjectsClient.resolve.mockResolvedValue({ + saved_object: { + id: 'food', + type: 'query', + attributes: { + title: 'food', + description: 'bar', + query: { + language: 'kuery', + query: '"null"', + }, + }, + references: [], + }, + outcome: 'exactMatch', + }); + + const response = await context.get('food'); + expect(response.attributes.query.query).toEqual('"null"'); + }); + + it('should not lose quotes', async () => { + mockSavedObjectsClient.resolve.mockResolvedValue({ + saved_object: { + id: 'food', + type: 'query', + attributes: { + title: 'food', + description: 'bar', + query: { + language: 'kuery', + query: '"Bob"', + }, + }, + references: [], + }, + outcome: 'exactMatch', + }); + + const response = await context.get('food'); + expect(response.attributes.query.query).toEqual('"Bob"'); + }); + + it('should inject references', async () => { + mockSavedObjectsClient.resolve.mockResolvedValue({ + saved_object: { + id: 'food', + type: 'query', + attributes: savedQueryAttributesWithFilters, + references: [ + { + id: 'my-new-index', + type: DATA_VIEW_SAVED_OBJECT_TYPE, + name: 'my-index', + }, + ], + }, + outcome: 'exactMatch', + }); + + const response = await context.get('food'); + expect(response.attributes.filters[0].meta.index).toBe('my-new-index'); + }); + + it('should throw if conflict', async () => { + mockSavedObjectsClient.resolve.mockResolvedValue({ + saved_object: { + id: 'foo', + type: 'query', + attributes: savedQueryAttributes, + references: [], + }, + outcome: 'conflict', + }); + + const result = context.get('food'); + expect(result).rejects.toMatchInlineSnapshot( + `[Error: Multiple saved queries found with ID: food (legacy URL alias conflict)]` + ); + }); + }); + + describe('delete', function () { + it('should delete the saved query for the given ID', async () => { + await context.delete('foo'); + expect(mockSavedObjectsClient.delete).toHaveBeenCalledWith('query', 'foo'); + }); + }); + + describe('count', function () { + it('should return the total number of saved queries', async () => { + mockSavedObjectsClient.find.mockResolvedValue({ + total: 1, + page: 0, + per_page: 0, + saved_objects: [], + }); + + const response = await context.count(); + + expect(response).toEqual(1); + }); + }); +}); diff --git a/src/plugins/data/server/query/route_handler_context.ts b/src/plugins/data/server/query/route_handler_context.ts new file mode 100644 index 0000000000000..3c60b33559b72 --- /dev/null +++ b/src/plugins/data/server/query/route_handler_context.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 { RequestHandlerContext, SavedObject } from 'kibana/server'; +import { isFilters } from '@kbn/es-query'; +import { isQuery, SavedQueryAttributes } from '../../common'; +import { extract, inject } from '../../common/query/persistable_state'; + +function injectReferences({ + id, + attributes, + references, +}: Pick, 'id' | 'attributes' | 'references'>) { + const { query } = attributes; + if (typeof query.query === 'string') { + try { + const parsed = JSON.parse(query.query); + query.query = parsed instanceof Object ? parsed : query.query; + } catch (e) { + // Just keep it as a string + } + } + const filters = inject(attributes.filters ?? [], references); + return { id, attributes: { ...attributes, filters } }; +} + +function extractReferences({ + title, + description, + query, + filters = [], + timefilter, +}: SavedQueryAttributes) { + const { state: extractedFilters, references } = extract(filters); + + const attributes: SavedQueryAttributes = { + title: title.trim(), + description: description.trim(), + query: { + ...query, + query: typeof query.query === 'string' ? query.query : JSON.stringify(query.query), + }, + filters: extractedFilters, + ...(timefilter && { timefilter }), + }; + + return { attributes, references }; +} + +function verifySavedQuery({ title, query, filters = [] }: SavedQueryAttributes) { + if (!isQuery(query)) { + throw new Error(`Invalid query: ${query}`); + } + + if (!isFilters(filters)) { + throw new Error(`Invalid filters: ${filters}`); + } + + if (!title.trim().length) { + throw new Error('Cannot create saved query without a title'); + } +} + +export function registerSavedQueryRouteHandlerContext(context: RequestHandlerContext) { + const createSavedQuery = async (attrs: SavedQueryAttributes) => { + verifySavedQuery(attrs); + const { attributes, references } = extractReferences(attrs); + + const savedObject = await context.core.savedObjects.client.create( + 'query', + attributes, + { + references, + } + ); + + // TODO: Handle properly + if (savedObject.error) throw new Error(savedObject.error.message); + + return injectReferences(savedObject); + }; + + const updateSavedQuery = async (id: string, attrs: SavedQueryAttributes) => { + verifySavedQuery(attrs); + const { attributes, references } = extractReferences(attrs); + + const savedObject = await context.core.savedObjects.client.update( + 'query', + id, + attributes, + { + references, + } + ); + + // TODO: Handle properly + if (savedObject.error) throw new Error(savedObject.error.message); + + return injectReferences({ id, attributes, references }); + }; + + const getSavedQuery = async (id: string) => { + const { saved_object: savedObject, outcome } = + await context.core.savedObjects.client.resolve('query', id); + if (outcome === 'conflict') { + throw new Error(`Multiple saved queries found with ID: ${id} (legacy URL alias conflict)`); + } else if (savedObject.error) { + throw new Error(savedObject.error.message); + } + return injectReferences(savedObject); + }; + + const getSavedQueriesCount = async () => { + const { total } = await context.core.savedObjects.client.find({ + type: 'query', + }); + return total; + }; + + const findSavedQueries = async ({ page = 1, perPage = 50, search = '' } = {}) => { + const { total, saved_objects: savedObjects } = + await context.core.savedObjects.client.find({ + type: 'query', + page, + perPage, + search, + }); + + const savedQueries = savedObjects.map(injectReferences); + + return { total, savedQueries }; + }; + + const deleteSavedQuery = (id: string) => { + return context.core.savedObjects.client.delete('query', id); + }; + + return { + create: createSavedQuery, + update: updateSavedQuery, + get: getSavedQuery, + count: getSavedQueriesCount, + find: findSavedQueries, + delete: deleteSavedQuery, + }; +} + +export interface SavedQueryRouteHandlerContext extends RequestHandlerContext { + savedQuery: ReturnType; +} diff --git a/src/plugins/data/server/query/routes.ts b/src/plugins/data/server/query/routes.ts new file mode 100644 index 0000000000000..cdf9e6f43dccc --- /dev/null +++ b/src/plugins/data/server/query/routes.ts @@ -0,0 +1,144 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { CoreSetup } from 'kibana/server'; +import { SavedQueryRouteHandlerContext } from './route_handler_context'; + +const SAVED_QUERY_PATH = '/api/saved_query'; +const SAVED_QUERY_ID_CONFIG = schema.object({ + id: schema.string(), +}); +const SAVED_QUERY_ATTRS_CONFIG = schema.object({ + title: schema.string(), + description: schema.string(), + query: schema.object({ + query: schema.oneOf([schema.string(), schema.object({}, { unknowns: 'allow' })]), + language: schema.string(), + }), + filters: schema.maybe(schema.arrayOf(schema.any())), + timefilter: schema.maybe(schema.any()), +}); + +export function registerSavedQueryRoutes({ http }: CoreSetup): void { + const router = http.createRouter(); + + router.post( + { + path: `${SAVED_QUERY_PATH}/_create`, + validate: { + body: SAVED_QUERY_ATTRS_CONFIG, + }, + }, + async (context, request, response) => { + try { + const body = await context.savedQuery.create(request.body); + return response.ok({ body }); + } catch (e) { + // TODO: Handle properly + return response.customError(e); + } + } + ); + + router.put( + { + path: `${SAVED_QUERY_PATH}/{id}`, + validate: { + params: SAVED_QUERY_ID_CONFIG, + body: SAVED_QUERY_ATTRS_CONFIG, + }, + }, + async (context, request, response) => { + const { id } = request.params; + try { + const body = await context.savedQuery.update(id, request.body); + return response.ok({ body }); + } catch (e) { + // TODO: Handle properly + return response.customError(e); + } + } + ); + + router.get( + { + path: `${SAVED_QUERY_PATH}/{id}`, + validate: { + params: SAVED_QUERY_ID_CONFIG, + }, + }, + async (context, request, response) => { + const { id } = request.params; + try { + const body = await context.savedQuery.get(id); + return response.ok({ body }); + } catch (e) { + // TODO: Handle properly + return response.customError(e); + } + } + ); + + router.get( + { + path: `${SAVED_QUERY_PATH}/_count`, + validate: {}, + }, + async (context, request, response) => { + try { + const count = await context.savedQuery.count(); + return response.ok({ body: `${count}` }); + } catch (e) { + // TODO: Handle properly + return response.customError(e); + } + } + ); + + router.post( + { + path: `${SAVED_QUERY_PATH}/_find`, + validate: { + body: schema.object({ + search: schema.string({ defaultValue: '' }), + perPage: schema.number({ defaultValue: 50 }), + page: schema.number({ defaultValue: 1 }), + }), + }, + }, + async (context, request, response) => { + try { + const body = await context.savedQuery.find(request.body); + return response.ok({ body }); + } catch (e) { + // TODO: Handle properly + return response.customError(e); + } + } + ); + + router.delete( + { + path: `${SAVED_QUERY_PATH}/{id}`, + validate: { + params: SAVED_QUERY_ID_CONFIG, + }, + }, + async (context, request, response) => { + const { id } = request.params; + try { + const body = await context.savedQuery.delete(id); + return response.ok({ body }); + } catch (e) { + // TODO: Handle properly + return response.customError(e); + } + } + ); +} diff --git a/src/plugins/data/server/saved_objects/migrations/query.ts b/src/plugins/data/server/saved_objects/migrations/query.ts new file mode 100644 index 0000000000000..9640725e3edd4 --- /dev/null +++ b/src/plugins/data/server/saved_objects/migrations/query.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { mapValues } from 'lodash'; +import { SavedObject } from 'kibana/server'; +import { SavedQueryAttributes } from '../../../common'; +import { extract, getAllMigrations } from '../../../common/query/persistable_state'; +import { mergeMigrationFunctionMaps } from '../../../../kibana_utils/common'; + +const extractFilterReferences = (doc: SavedObject) => { + const { state: filters, references } = extract(doc.attributes.filters ?? []); + return { + ...doc, + attributes: { + ...doc.attributes, + filters, + }, + references, + }; +}; + +const filterMigrations = mapValues(getAllMigrations(), (migrate) => { + return (doc: SavedObject) => ({ + ...doc, + attributes: { + ...doc.attributes, + filters: migrate(doc.attributes.filters), + }, + }); +}); + +export const savedQueryMigrations = mergeMigrationFunctionMaps( + { + '7.16.0': extractFilterReferences, + }, + filterMigrations +); diff --git a/src/plugins/data/server/saved_objects/query.ts b/src/plugins/data/server/saved_objects/query.ts index bc6225255b5e6..6fd34f4802726 100644 --- a/src/plugins/data/server/saved_objects/query.ts +++ b/src/plugins/data/server/saved_objects/query.ts @@ -7,6 +7,7 @@ */ import { SavedObjectsType } from 'kibana/server'; +import { savedQueryMigrations } from './migrations/query'; export const querySavedObjectType: SavedObjectsType = { name: 'query', @@ -38,5 +39,5 @@ export const querySavedObjectType: SavedObjectsType = { timefilter: { type: 'object', enabled: false }, }, }, - migrations: {}, + migrations: savedQueryMigrations, }; diff --git a/x-pack/plugins/security_solution/public/common/utils/saved_query_services/index.tsx b/x-pack/plugins/security_solution/public/common/utils/saved_query_services/index.tsx index b15a466af4d79..a5c4d81b1728c 100644 --- a/x-pack/plugins/security_solution/public/common/utils/saved_query_services/index.tsx +++ b/x-pack/plugins/security_solution/public/common/utils/saved_query_services/index.tsx @@ -15,14 +15,14 @@ import { useKibana } from '../../lib/kibana'; export const useSavedQueryServices = () => { const kibana = useKibana(); - const client = kibana.services.savedObjects.client; + const { http } = kibana.services; const [savedQueryService, setSavedQueryService] = useState( - createSavedQueryService(client) + createSavedQueryService(http) ); useEffect(() => { - setSavedQueryService(createSavedQueryService(client)); - }, [client]); + setSavedQueryService(createSavedQueryService(http)); + }, [http]); return savedQueryService; }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx index b2b304e16c4a0..daafec3005eb8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx @@ -244,27 +244,19 @@ export const QueryBarTimeline = memo( (f) => f.meta.controlledBy === TIMELINE_FILTER_DROP_AREA ) : -1; - savedQueryServices.saveQuery( - { - ...newSavedQuery.attributes, - filters: - newSavedQuery.attributes.filters != null - ? dataProviderFilterExists > -1 - ? [ - ...newSavedQuery.attributes.filters.slice(0, dataProviderFilterExists), - getDataProviderFilter(dataProvidersDsl), - ...newSavedQuery.attributes.filters.slice(dataProviderFilterExists + 1), - ] - : [ - ...newSavedQuery.attributes.filters, - getDataProviderFilter(dataProvidersDsl), - ] - : [], - }, - { - overwrite: true, - } - ); + savedQueryServices.updateQuery(newSavedQuery.id, { + ...newSavedQuery.attributes, + filters: + newSavedQuery.attributes.filters != null + ? dataProviderFilterExists > -1 + ? [ + ...newSavedQuery.attributes.filters.slice(0, dataProviderFilterExists), + getDataProviderFilter(dataProvidersDsl), + ...newSavedQuery.attributes.filters.slice(dataProviderFilterExists + 1), + ] + : [...newSavedQuery.attributes.filters, getDataProviderFilter(dataProvidersDsl)] + : [], + }); } } else { setSavedQueryId(null); From e578bf1f68c9ba223122e60b5d03bc24def0a2b7 Mon Sep 17 00:00:00 2001 From: Mark Hopkin Date: Thu, 21 Oct 2021 17:45:58 +0100 Subject: [PATCH 14/40] remove hash router related code (#115733) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../create_package_policy_page/index.test.tsx | 14 ------ .../create_package_policy_page/index.tsx | 44 ++++--------------- 2 files changed, 9 insertions(+), 49 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.test.tsx index c115089cccb1e..b8bae0cb1f541 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.test.tsx @@ -71,20 +71,6 @@ describe('when on the package policy create page', () => { expect(cancelLink.href).toBe(expectedRouteState.onCancelUrl); expect(cancelButton.href).toBe(expectedRouteState.onCancelUrl); }); - - it('should redirect via history when cancel link is clicked', () => { - act(() => { - cancelLink.click(); - }); - expect(testRenderer.mountHistory.location.pathname).toBe('/cancel/url/here'); - }); - - it('should redirect via history when cancel Button (button bar) is clicked', () => { - act(() => { - cancelButton.click(); - }); - expect(testRenderer.mountHistory.location.pathname).toBe('/cancel/url/here'); - }); }); }); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx index f6ad41f69e99e..b30d51bb46aaa 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx @@ -22,7 +22,6 @@ import { EuiErrorBoundary, } from '@elastic/eui'; import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; -import type { ApplicationStart } from 'kibana/public'; import { safeLoad } from 'js-yaml'; import type { @@ -46,7 +45,6 @@ import { ConfirmDeployAgentPolicyModal } from '../components'; import { useIntraAppState, useUIExtension } from '../../../hooks'; import { ExtensionWrapper } from '../../../components'; import type { PackagePolicyEditExtensionComponentProps } from '../../../types'; -import { PLUGIN_ID } from '../../../../../../common/constants'; import { pkgKeyFromPackageInfo } from '../../../services'; import { CreatePackagePolicyPageLayout, PostInstallAddAgentModal } from './components'; @@ -76,14 +74,16 @@ interface AddToPolicyParams { } export const CreatePackagePolicyPage: React.FunctionComponent = () => { - const { notifications } = useStartServices(); + const { + application: { navigateToApp }, + notifications, + } = useStartServices(); const { agents: { enabled: isFleetEnabled }, } = useConfig(); const { params } = useRouteMatch(); const { getHref, getPath } = useLink(); const history = useHistory(); - const handleNavigateTo = useNavigateToCallback(); const routeState = useIntraAppState(); const { search } = useLocation(); @@ -254,10 +254,10 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { (ev) => { if (routeState && routeState.onCancelNavigateTo) { ev.preventDefault(); - handleNavigateTo(routeState.onCancelNavigateTo); + navigateToApp(...routeState.onCancelNavigateTo); } }, - [routeState, handleNavigateTo] + [routeState, navigateToApp] ); // Save package policy @@ -298,15 +298,15 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { mappingOptions: routeState.onSaveQueryParams, paramsToApply, }); - handleNavigateTo([appId, { ...options, path: pathWithQueryString }]); + navigateToApp(appId, { ...options, path: pathWithQueryString }); } else { - handleNavigateTo(routeState.onSaveNavigateTo); + navigateToApp(...routeState.onSaveNavigateTo); } } else { history.push(getPath('policy_details', { policyId: agentPolicy!.id })); } }, - [agentPolicy, getPath, handleNavigateTo, history, routeState] + [agentPolicy, getPath, navigateToApp, history, routeState] ); const onSubmit = useCallback(async () => { @@ -578,29 +578,3 @@ const IntegrationBreadcrumb: React.FunctionComponent<{ }); return null; }; - -const useNavigateToCallback = () => { - const history = useHistory(); - const { - application: { navigateToApp }, - } = useStartServices(); - - return useCallback( - (navigateToProps: Parameters) => { - // If navigateTo appID is `fleet`, then don't use Kibana's navigateTo method, because that - // uses BrowserHistory but within fleet, we are using HashHistory. - // This temporary workaround hook can be removed once this issue is addressed: - // https://github.com/elastic/kibana/issues/70358 - if (navigateToProps[0] === PLUGIN_ID) { - const { path = '', state } = navigateToProps[1] || {}; - history.push({ - pathname: path.charAt(0) === '#' ? path.substr(1) : path, - state, - }); - } - - return navigateToApp(...navigateToProps); - }, - [history, navigateToApp] - ); -}; From c53004fe0675ba8c692c62f3bf0a8b067f393008 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Thu, 21 Oct 2021 13:08:06 -0400 Subject: [PATCH 15/40] [Security Solution] Fix metadata tests with updated timestamps (#115591) --- .../endpoint/metadata/api_feature/data.json | 36 +++++++++---------- .../apis/metadata.ts | 7 ++-- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json b/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json index b3d33f5d45345..449731d9e4ab2 100644 --- a/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json +++ b/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json @@ -4,7 +4,7 @@ "id": "3KVN2G8BYQH1gtPUuYk7", "index": "metrics-endpoint.metadata-default", "source": { - "@timestamp": 1626897841950, + "@timestamp": 1634656952181, "agent": { "id": "963b081e-60d1-482c-befd-a5815fa8290f", "version": "6.6.1", @@ -26,7 +26,7 @@ } }, "event": { - "created": 1626897841950, + "created": 1634656952181, "id": "32f5fda2-48e4-4fae-b89e-a18038294d14", "kind": "metric", "category": [ @@ -74,7 +74,7 @@ "id": "3aVN2G8BYQH1gtPUuYk7", "index": "metrics-endpoint.metadata-default", "source": { - "@timestamp": 1626897841950, + "@timestamp": 1634656952181, "agent": { "id": "b3412d6f-b022-4448-8fee-21cc936ea86b", "version": "6.0.0", @@ -96,7 +96,7 @@ } }, "event": { - "created": 1626897841950, + "created": 1634656952181, "id": "32f5fda2-48e4-4fae-b89e-a18038294d15", "kind": "metric", "category": [ @@ -143,7 +143,7 @@ "id": "3qVN2G8BYQH1gtPUuYk7", "index": "metrics-endpoint.metadata-default", "source": { - "@timestamp": 1626897841950, + "@timestamp": 1634656952181, "agent": { "id": "3838df35-a095-4af4-8fce-0b6d78793f2e", "version": "6.8.0", @@ -165,7 +165,7 @@ } }, "event": { - "created": 1626897841950, + "created": 1634656952181, "id": "32f5fda2-48e4-4fae-b89e-a18038294d16", "kind": "metric", "category": [ @@ -210,7 +210,7 @@ "id": "36VN2G8BYQH1gtPUuYk7", "index": "metrics-endpoint.metadata-default", "source": { - "@timestamp": 1626897841950, + "@timestamp": 1634656952181, "agent": { "id": "963b081e-60d1-482c-befd-a5815fa8290f", "version": "6.6.1", @@ -232,7 +232,7 @@ } }, "event": { - "created": 1626897841950, + "created": 1634656952181, "id": "32f5fda2-48e4-4fae-b89e-a18038294d18", "kind": "metric", "category": [ @@ -280,7 +280,7 @@ "id": "4KVN2G8BYQH1gtPUuYk7", "index": "metrics-endpoint.metadata-default", "source": { - "@timestamp": 1626897841950, + "@timestamp": 1634656952181, "agent": { "id": "b3412d6f-b022-4448-8fee-21cc936ea86b", "version": "6.0.0", @@ -302,7 +302,7 @@ } }, "event": { - "created": 1626897841950, + "created": 1634656952181, "id": "32f5fda2-48e4-4fae-b89e-a18038294d19", "kind": "metric", "category": [ @@ -348,7 +348,7 @@ "id": "4aVN2G8BYQH1gtPUuYk7", "index": "metrics-endpoint.metadata-default", "source": { - "@timestamp": 1626897841950, + "@timestamp": 1634656952181, "agent": { "id": "3838df35-a095-4af4-8fce-0b6d78793f2e", "version": "6.8.0", @@ -370,7 +370,7 @@ } }, "event": { - "created": 1626897841950, + "created": 1634656952181, "id": "32f5fda2-48e4-4fae-b89e-a18038294d39", "kind": "metric", "category": [ @@ -416,7 +416,7 @@ "id": "4qVN2G8BYQH1gtPUuYk7", "index": "metrics-endpoint.metadata-default", "source": { - "@timestamp": 1626897841950, + "@timestamp": 1634656952181, "agent": { "id": "963b081e-60d1-482c-befd-a5815fa8290f", "version": "6.6.1", @@ -438,7 +438,7 @@ } }, "event": { - "created": 1626897841950, + "created": 1634656952181, "id": "32f5fda2-48e4-4fae-b89e-a18038294d31", "kind": "metric", "category": [ @@ -485,7 +485,7 @@ "id": "46VN2G8BYQH1gtPUuYk7", "index": "metrics-endpoint.metadata-default", "source": { - "@timestamp": 1626897841950, + "@timestamp": 1634656952181, "agent": { "id": "b3412d6f-b022-4448-8fee-21cc936ea86b", "version": "6.0.0", @@ -507,7 +507,7 @@ } }, "event": { - "created": 1626897841950, + "created": 1634656952181, "id": "32f5fda2-48e4-4fae-b89e-a18038294d23", "kind": "metric", "category": [ @@ -553,7 +553,7 @@ "id": "5KVN2G8BYQH1gtPUuYk7", "index": "metrics-endpoint.metadata-default", "source": { - "@timestamp": 1626897841950, + "@timestamp": 1634656952181, "agent": { "id": "3838df35-a095-4af4-8fce-0b6d78793f2e", "version": "6.8.0", @@ -575,7 +575,7 @@ } }, "event": { - "created": 1626897841950, + "created": 1634656952181, "id": "32f5fda2-48e4-4fae-b89e-a18038294d35", "kind": "metric", "category": [ diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts index 2dcf36cc42ae2..afdc364ffd970 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts @@ -24,8 +24,7 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); - // Failing: See https://github.com/elastic/kibana/issues/115488 - describe.skip('test metadata api', () => { + describe('test metadata api', () => { // TODO add this after endpoint package changes are merged and in snapshot // describe('with .metrics-endpoint.metadata_united_default index', () => { // }); @@ -242,7 +241,7 @@ export default function ({ getService }: FtrProviderContext) { (ip: string) => ip === targetEndpointIp ); expect(resultIp).to.eql([targetEndpointIp]); - expect(body.hosts[0].metadata.event.created).to.eql(1626897841950); + expect(body.hosts[0].metadata.event.created).to.eql(1634656952181); expect(body.hosts.length).to.eql(1); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); @@ -284,7 +283,7 @@ export default function ({ getService }: FtrProviderContext) { const resultElasticAgentId: string = body.hosts[0].metadata.elastic.agent.id; expect(resultHostId).to.eql(targetEndpointId); expect(resultElasticAgentId).to.eql(targetElasticAgentId); - expect(body.hosts[0].metadata.event.created).to.eql(1626897841950); + expect(body.hosts[0].metadata.event.created).to.eql(1634656952181); expect(body.hosts[0].host_status).to.eql('unhealthy'); expect(body.hosts.length).to.eql(1); expect(body.request_page_size).to.eql(10); From 8bee1b8f4194012a1ac8efe14370843bce304a27 Mon Sep 17 00:00:00 2001 From: Bhavya RM Date: Thu, 21 Oct 2021 13:11:18 -0400 Subject: [PATCH 16/40] unskip kibana overview test from xpack and add the missing painless labs test to config.ts (#115240) --- x-pack/test/accessibility/apps/kibana_overview.ts | 2 +- x-pack/test/accessibility/config.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/test/accessibility/apps/kibana_overview.ts b/x-pack/test/accessibility/apps/kibana_overview.ts index 9f5d91e5b4d54..6ea51cc0b855c 100644 --- a/x-pack/test/accessibility/apps/kibana_overview.ts +++ b/x-pack/test/accessibility/apps/kibana_overview.ts @@ -12,7 +12,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); // FLAKY: https://github.com/elastic/kibana/issues/98463 - describe.skip('Kibana overview', () => { + describe('Kibana overview', () => { const esArchiver = getService('esArchiver'); before(async () => { diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index 699b5b48d604c..933e8e97da397 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -17,10 +17,11 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { testFiles: [ require.resolve('./apps/login_page'), - require.resolve('./apps/home'), require.resolve('./apps/kibana_overview'), + require.resolve('./apps/home'), require.resolve('./apps/grok_debugger'), require.resolve('./apps/search_profiler'), + require.resolve('./apps/painless_lab'), require.resolve('./apps/uptime'), require.resolve('./apps/spaces'), require.resolve('./apps/advanced_settings'), From 537bce70ef32f91bf501c225fc997811ccf3eef2 Mon Sep 17 00:00:00 2001 From: Byron Hulcher Date: Thu, 21 Oct 2021 13:20:39 -0400 Subject: [PATCH 17/40] [App Search] Improve loading experience on the Curation and Curation Detail pages (#115840) --- .../curations/curation/automated_curation.test.tsx | 12 +----------- .../curations/curation/automated_curation.tsx | 7 +++---- .../components/curations/curation/curation.test.tsx | 9 +++++++++ .../components/curations/curation/curation.tsx | 7 ++++++- .../curations/curation/manual_curation.test.tsx | 1 - .../curations/curation/manual_curation.tsx | 3 +-- .../components/curations/views/curations.test.tsx | 6 ++++-- .../components/curations/views/curations.tsx | 6 ++++-- 8 files changed, 28 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.test.tsx index 309924e99f600..960018d92e54e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.test.tsx @@ -15,7 +15,7 @@ import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; -import { EuiBadge, EuiButton, EuiLoadingSpinner, EuiTab } from '@elastic/eui'; +import { EuiBadge, EuiButton, EuiTab } from '@elastic/eui'; import { getPageHeaderActions, getPageHeaderTabs, getPageTitle } from '../../../../test_helpers'; @@ -32,7 +32,6 @@ import { History } from './history'; describe('AutomatedCuration', () => { const values = { - dataLoading: false, queries: ['query A', 'query B'], isFlyoutOpen: false, curation: { @@ -115,15 +114,6 @@ describe('AutomatedCuration', () => { expect(pageTitle.find(EuiBadge)).toHaveLength(1); }); - it('displays a spinner in the title when loading', () => { - setMockValues({ ...values, dataLoading: true }); - - const wrapper = shallow(); - const pageTitle = shallow(
{getPageTitle(wrapper)}
); - - expect(pageTitle.find(EuiLoadingSpinner)).toHaveLength(1); - }); - it('contains a button to delete the curation', () => { const wrapper = shallow(); const pageHeaderActions = getPageHeaderActions(wrapper); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.tsx index eefe012cd8a28..24406a0372a44 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.tsx @@ -10,7 +10,7 @@ import { useParams } from 'react-router-dom'; import { useValues, useActions } from 'kea'; -import { EuiButton, EuiBadge, EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiButton, EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EngineLogic } from '../../engine'; @@ -35,7 +35,7 @@ export const AutomatedCuration: React.FC = () => { const { curationId } = useParams<{ curationId: string }>(); const logic = CurationLogic({ curationId }); const { convertToManual, onSelectPageTab } = useActions(logic); - const { activeQuery, dataLoading, queries, curation, selectedPageTab } = useValues(logic); + const { activeQuery, queries, curation, selectedPageTab } = useValues(logic); const { engineName } = useValues(EngineLogic); const pageTabs = [ @@ -69,7 +69,7 @@ export const AutomatedCuration: React.FC = () => { pageHeader={{ pageTitle: ( <> - {dataLoading ? : activeQuery}{' '} + {activeQuery}{' '} {AUTOMATED_LABEL} @@ -96,7 +96,6 @@ export const AutomatedCuration: React.FC = () => { ], tabs: pageTabs, }} - isLoading={dataLoading} > {selectedPageTab === 'promoted' && } {selectedPageTab === 'promoted' && } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx index 62c3a6c7d4578..dce56a05f8f8c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx @@ -14,6 +14,7 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { EnterpriseSearchPageTemplate } from '../../../../shared/layout'; import { rerender } from '../../../../test_helpers'; jest.mock('./curation_logic', () => ({ CurationLogic: jest.fn() })); @@ -26,6 +27,7 @@ import { Curation } from './'; describe('Curation', () => { const values = { + dataLoading: false, isAutomated: true, }; @@ -49,6 +51,13 @@ describe('Curation', () => { expect(actions.loadCuration).toHaveBeenCalledTimes(2); }); + it('renders a loading view when loading', () => { + setMockValues({ dataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.is(EnterpriseSearchPageTemplate)).toBe(true); + }); + it('renders a view for automated curations', () => { setMockValues({ isAutomated: true }); const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx index 19b6542e96c4b..d1b0f43d976a8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx @@ -10,6 +10,8 @@ import { useParams } from 'react-router-dom'; import { useValues, useActions } from 'kea'; +import { EnterpriseSearchPageTemplate } from '../../../../shared/layout'; + import { AutomatedCuration } from './automated_curation'; import { CurationLogic } from './curation_logic'; import { ManualCuration } from './manual_curation'; @@ -17,11 +19,14 @@ import { ManualCuration } from './manual_curation'; export const Curation: React.FC = () => { const { curationId } = useParams() as { curationId: string }; const { loadCuration } = useActions(CurationLogic({ curationId })); - const { isAutomated } = useValues(CurationLogic({ curationId })); + const { dataLoading, isAutomated } = useValues(CurationLogic({ curationId })); useEffect(() => { loadCuration(); }, [curationId]); + if (dataLoading) { + return ; + } return isAutomated ? : ; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.test.tsx index d739eae55040d..548d111d6f96e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.test.tsx @@ -30,7 +30,6 @@ import { SuggestedDocumentsCallout } from './suggested_documents_callout'; describe('ManualCuration', () => { const values = { - dataLoading: false, queries: ['query A', 'query B'], isFlyoutOpen: false, selectedPageTab: 'promoted', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.tsx index e78a80a5878b8..45b1b6212f504 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.tsx @@ -28,7 +28,7 @@ export const ManualCuration: React.FC = () => { const { curationId } = useParams() as { curationId: string }; const logic = CurationLogic({ curationId }); const { onSelectPageTab } = useActions(logic); - const { dataLoading, queries, selectedPageTab, curation } = useValues(logic); + const { queries, selectedPageTab, curation } = useValues(logic); const { isFlyoutOpen } = useValues(AddResultLogic); @@ -64,7 +64,6 @@ export const ManualCuration: React.FC = () => { ], tabs: pageTabs, }} - isLoading={dataLoading} > {selectedPageTab === 'promoted' && } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx index f446438d83d94..4e09dadc6c836 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx @@ -166,18 +166,20 @@ describe('Curations', () => { }); describe('loading state', () => { - it('renders a full-page loading state on initial page load', () => { + it('renders a full-page loading state and hides tabs on initial page load', () => { setMockValues({ ...values, dataLoading: true }); const wrapper = shallow(); expect(wrapper.prop('isLoading')).toEqual(true); + expect(wrapper.prop('tabs')).toBeUndefined(); }); - it('does not re-render a full-page loading state when data is loaded', () => { + it('does not re-render a full-page loading and shows tabs state when data is loaded', () => { setMockValues({ ...values, dataLoading: false }); const wrapper = shallow(); expect(wrapper.prop('isLoading')).toEqual(false); + expect(typeof wrapper.prop('tabs')).not.toBeUndefined(); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx index e5b064e649af0..1cd8313743536 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx @@ -83,6 +83,8 @@ export const Curations: React.FC = () => { loadCurations(); }, [meta.page.current]); + const isLoading = curationsSettingsDataLoading || curationsDataLoading; + return ( { {CREATE_NEW_CURATION_TITLE} , ], - tabs: pageTabs, + tabs: isLoading ? undefined : pageTabs, }} - isLoading={curationsSettingsDataLoading || curationsDataLoading} + isLoading={isLoading} > {selectedPageTab === 'overview' && } {selectedPageTab === 'history' && } From 7f83ec09d602517abba7b8fd6a4d7fb16d7269bd Mon Sep 17 00:00:00 2001 From: Byron Hulcher Date: Thu, 21 Oct 2021 13:21:06 -0400 Subject: [PATCH 18/40] [App Search] Restrict log stream on Automated Curation Detail page to only automated events (#115841) --- .../curations/curation/automated_curation.test.tsx | 4 ++-- .../components/curations/curation/automated_curation.tsx | 4 ++-- ...story.test.tsx => automated_curation_history.test.tsx} | 8 ++++---- .../{history.tsx => automated_curation_history.tsx} | 3 ++- 4 files changed, 10 insertions(+), 9 deletions(-) rename x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/{history.test.tsx => automated_curation_history.test.tsx} (68%) rename x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/{history.tsx => automated_curation_history.tsx} (90%) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.test.tsx index 960018d92e54e..ddc9c69a35c8d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.test.tsx @@ -24,11 +24,11 @@ jest.mock('./curation_logic', () => ({ CurationLogic: jest.fn() })); import { AppSearchPageTemplate } from '../../layout'; import { AutomatedCuration } from './automated_curation'; +import { AutomatedCurationHistory } from './automated_curation_history'; import { CurationLogic } from './curation_logic'; import { DeleteCurationButton } from './delete_curation_button'; import { PromotedDocuments, OrganicDocuments } from './documents'; -import { History } from './history'; describe('AutomatedCuration', () => { const values = { @@ -96,7 +96,7 @@ describe('AutomatedCuration', () => { expect(tabs.at(2).prop('isSelected')).toEqual(true); - expect(wrapper.find(History)).toHaveLength(1); + expect(wrapper.find(AutomatedCurationHistory)).toHaveLength(1); }); it('initializes CurationLogic with a curationId prop from URL param', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.tsx index 24406a0372a44..0351d4c113d13 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.tsx @@ -25,11 +25,11 @@ import { import { getCurationsBreadcrumbs } from '../utils'; +import { AutomatedCurationHistory } from './automated_curation_history'; import { HIDDEN_DOCUMENTS_TITLE, PROMOTED_DOCUMENTS_TITLE } from './constants'; import { CurationLogic } from './curation_logic'; import { DeleteCurationButton } from './delete_curation_button'; import { PromotedDocuments, OrganicDocuments } from './documents'; -import { History } from './history'; export const AutomatedCuration: React.FC = () => { const { curationId } = useParams<{ curationId: string }>(); @@ -100,7 +100,7 @@ export const AutomatedCuration: React.FC = () => { {selectedPageTab === 'promoted' && } {selectedPageTab === 'promoted' && } {selectedPageTab === 'history' && ( - + )} ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/history.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation_history.test.tsx similarity index 68% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/history.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation_history.test.tsx index a7f83fb0c61d9..b7d1b6f9ed809 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/history.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation_history.test.tsx @@ -11,13 +11,13 @@ import { shallow } from 'enzyme'; import { EntSearchLogStream } from '../../../../shared/log_stream'; -import { History } from './history'; +import { AutomatedCurationHistory } from './automated_curation_history'; -describe('History', () => { +describe('AutomatedCurationHistory', () => { it('renders', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(EntSearchLogStream).prop('query')).toEqual( - 'appsearch.search_relevance_suggestions.query: some text and event.kind: event and event.dataset: search-relevance-suggestions and appsearch.search_relevance_suggestions.engine: foo and event.action: curation_suggestion' + 'appsearch.search_relevance_suggestions.query: some text and event.kind: event and event.dataset: search-relevance-suggestions and appsearch.search_relevance_suggestions.engine: foo and event.action: curation_suggestion and appsearch.search_relevance_suggestions.suggestion.new_status: automated' ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/history.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation_history.tsx similarity index 90% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/history.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation_history.tsx index 744141372469c..f523beeb0a821 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/history.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation_history.tsx @@ -17,13 +17,14 @@ interface Props { engineName: string; } -export const History: React.FC = ({ query, engineName }) => { +export const AutomatedCurationHistory: React.FC = ({ query, engineName }) => { const filters = [ `appsearch.search_relevance_suggestions.query: ${query}`, 'event.kind: event', 'event.dataset: search-relevance-suggestions', `appsearch.search_relevance_suggestions.engine: ${engineName}`, 'event.action: curation_suggestion', + 'appsearch.search_relevance_suggestions.suggestion.new_status: automated', ]; return ( From 6600f1ad78afaf32e40281d82f0566c2490aad5c Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Thu, 21 Oct 2021 12:32:21 -0500 Subject: [PATCH 19/40] [ML] Add Index data visualizer grid embeddable as extra view within Discover (#107184) * [ML] Initial embed * [ML] Initial embed props * [ML] Add top nav link to data viz * Add visible fields * Add add data service to register links * Renames, refactor, use constants * Renames, refactor, use constants * Update tests and mocks * Embeddable * Update hook to update upon time udpate * Add filter support to query * Refactor filter utilities * Add filter support for embeddable * Fix saved search data undefined * Prototype aggregated view/document view switcher * Prototype flyout * Prototype save document view option in storage * Fix filter and query conflict with saved search * Minor styling edits * [ML] Initial embed * [ML] Initial embed props * Add embeddable 1 * Add visible fields * Embeddable 2 * Add filter support to query * Refactor filter utilities * Add filter support for embeddable * Fix saved search data undefined * Prototype aggregated view/document view switcher * Prototype flyout * Prototype save document view option in storage * Fix filter and query conflict with saved search * Minor styling edits * Fix missing code after conflicts * Remove dv locator and flyout * Make types happy * Fix types * Rename toggle option * Resolve conflicts * [ML] Reduce size of chart * [ML] Unbold name, switch icons of show distributions * [ML] Make size consistent * [ML] Make page size 25 * [ML] Switch to arrow right and down * [ML] Make legend font smaller * [ML] Add user setting * [ML] Add show preview by default setting * [ML] Match icon * Add panels around the subcontent * Add preference for aggregated vs doc * Fix types * Fix types, add constants for adv settings * Change to data view type * Temp fix for Kibana/EUI table overflow issue * Modify line height so text is not cut off, modify widths for varying screen sizes * Different width padders for different screens * Fix CI * Merge latest, move button to the right * Fix width for bar charts previews * Fix toggle buttons, fix maps * Delete unused file * Fix boolean styling * Change to enum, discover mode * Hide field stats * Hide field stats * Persist show mini preview/distribution settings * Remove window size, use size observer instead * Default to document view * Remove bold, switch icon * Set fixed width for top values, reduce font size in table * Fix custom url tests * Update width styling for panels * Fix missing flag for Discover sidebar, jest tests * Fix max width * Workaround for sorting * Fix import * Fix styling * Make height uniform, center alignment, fix map and keyword map not same size Move styling * Revert "Make height uniform, center alignment, fix map and keyword map not same size" This reverts commit 8fc42e2f * Revert "Make height uniform, center alignment, fix map and keyword map not same size" This reverts commit 8fc42e2f * Uniform height, left aligned, flex grid * Switch top values to have labels * Center content * Replace fixed widths with percentage * Fix table missing field types * Add dashboard embeddable and filter support * Fix file data viz styling and tests, lean up imports, remove hard coded pixels * Add search panel/kql filter bar * Temporarily fix scrolling * New kql filters for data visualizer * Set map height so it will fit the sampler shard size text * Use eui progress labels * Fix spacer * Add beta badge * Temporarily fix scrolling * Fix grow for Top Values for * [ML] Update functional tests to reflect new arrow icons * [ML] Add filter buttons and KQL bars * [ML] Update filter bar onChange behavior * [ML] Update top values filter onChange behavior * [ML] Update search filters when opening saved search * [ML] Clean up * [ML] Remove fit content for height * [ML] Fix boolean legend * [ML] Fix header section when browser width is small to large and when index pattern title is too large * [ML] Hide expander icon when dimension is xs or s & css fixes * [ML] Delete embeddables because they are not use * [ML] Rename view mode, refactor to separate hook, add error prompt if can't show, rename wrapper, clean up & fix tests * [ML] Make doc count 0 for empty fields, update t/f test * [ML] Add unit testing for search utils * Fix missing unsubscribe for embeddable output * Remove redundant onAddFilter for this PR, fix width * Rename Field Stats to Field stats to match convention * [ML] Fix expand all/collapse all behavior to override individual setting * [ML] Fix functional tests should be 0/0% * [ML] Fix docs content spacing, rename classnames, add filters to Discover, lens, and maps * [ML] Fix doc count for fields that exists but have no stats * [ML] Fix icon styling to match Discover but have text/keyword/histogram * [ML] Fix doc count for fields that exists but have no stats * [ML] Rename classnames to BEM style * Resolve latest changes * [ML] Add tests for data viz in Discover * Update tests & dashboard behavior to reflect new advanced settings * Update telemetry * Remove workaround after eui bump fix * Fix missing bool clause * Add login * Fix saved search attributes broken with latest changes * Update tests * Fix import * Match the no results found * Fix query util to return search source's results right away. Fix texts. * Rename old test suits to farequoteDataViewTestData Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/discover/common/index.ts | 1 + .../components/chart/discover_chart.test.tsx | 3 + .../main/components/chart/discover_chart.tsx | 17 + .../components/layout/discover_layout.scss | 4 + .../components/layout/discover_layout.tsx | 63 +- .../components/sidebar/discover_field.tsx | 42 +- .../sidebar/discover_field_visualize.tsx | 30 +- .../sidebar/discover_sidebar.test.tsx | 2 + .../components/sidebar/discover_sidebar.tsx | 11 + .../discover_sidebar_responsive.test.tsx | 2 + .../sidebar/discover_sidebar_responsive.tsx | 5 + .../components/top_nav/get_top_nav_links.ts | 3 +- .../components/view_mode_toggle/_index.scss | 1 + .../view_mode_toggle/_view_mode_toggle.scss | 12 + .../components/view_mode_toggle/constants.ts | 12 + .../main/components/view_mode_toggle/index.ts | 10 + .../view_mode_toggle/view_mode_toggle.tsx | 66 ++ .../apps/main/services/discover_state.ts | 9 + .../main/utils/get_state_defaults.test.ts | 4 + .../apps/main/utils/get_state_defaults.ts | 9 + .../apps/main/utils/persist_saved_search.ts | 8 + .../data_visualizer_grid.tsx | 196 ++++++ .../field_stats_table_embeddable.tsx | 31 + .../components/data_visualizer_grid/index.ts | 9 + .../embeddable/saved_search_embeddable.tsx | 31 +- src/plugins/discover/public/build_services.ts | 3 + src/plugins/discover/public/plugin.tsx | 5 + .../saved_searches/get_saved_searches.test.ts | 2 + .../saved_searches_utils.test.ts | 4 + .../saved_searches/saved_searches_utils.ts | 4 + .../discover/public/saved_searches/types.ts | 5 + .../discover/server/saved_objects/search.ts | 2 + src/plugins/discover/server/ui_settings.ts | 20 + .../server/collectors/management/schema.ts | 4 + .../server/collectors/management/types.ts | 1 + src/plugins/telemetry/schema/oss_plugins.json | 6 + test/functional/page_objects/discover_page.ts | 41 ++ .../field_type_icon.test.tsx.snap | 2 +- .../field_type_icon/field_type_icon.tsx | 2 +- .../application/common/util/url_state.tsx | 1 - .../embeddables/grid_embeddable/constants.ts | 8 + .../embeddable_loading_fallback.tsx | 20 + .../grid_embeddable/grid_embeddable.tsx | 234 +++++++ .../grid_embeddable_factory.tsx | 70 +++ .../embeddables/grid_embeddable/index.ts | 8 + .../use_data_visualizer_grid_data.ts | 587 ++++++++++++++++++ .../embeddables/index.ts | 24 + .../index_data_visualizer.tsx | 7 +- .../index_data_visualizer/locator/locator.ts | 2 - .../utils/saved_search_utils.test.ts | 10 +- .../utils/saved_search_utils.ts | 82 ++- .../plugins/data_visualizer/public/plugin.ts | 7 +- x-pack/plugins/data_visualizer/tsconfig.json | 11 +- .../configuration_step/use_saved_search.ts | 4 +- .../jobs/new_job/utils/new_job_utils.ts | 4 +- .../ml/public/application/util/index_utils.ts | 2 +- .../apps/ml/data_visualizer/index.ts | 3 +- .../data_visualizer/index_data_visualizer.ts | 372 +---------- .../index_data_visualizer_grid_in_discover.ts | 172 +++++ .../ml/data_visualizer/index_test_data.ts | 533 ++++++++++++++++ .../apps/ml/data_visualizer/types.ts | 47 ++ .../functional/services/ml/custom_urls.ts | 5 +- .../services/ml/data_visualizer_table.ts | 43 +- .../apps/ml/data_visualizer/index.ts | 5 + 64 files changed, 2469 insertions(+), 474 deletions(-) create mode 100644 src/plugins/discover/public/application/apps/main/components/view_mode_toggle/_index.scss create mode 100644 src/plugins/discover/public/application/apps/main/components/view_mode_toggle/_view_mode_toggle.scss create mode 100644 src/plugins/discover/public/application/apps/main/components/view_mode_toggle/constants.ts create mode 100644 src/plugins/discover/public/application/apps/main/components/view_mode_toggle/index.ts create mode 100644 src/plugins/discover/public/application/apps/main/components/view_mode_toggle/view_mode_toggle.tsx create mode 100644 src/plugins/discover/public/application/components/data_visualizer_grid/data_visualizer_grid.tsx create mode 100644 src/plugins/discover/public/application/components/data_visualizer_grid/field_stats_table_embeddable.tsx create mode 100644 src/plugins/discover/public/application/components/data_visualizer_grid/index.ts create mode 100644 x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/constants.ts create mode 100644 x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/embeddable_loading_fallback.tsx create mode 100644 x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable.tsx create mode 100644 x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable_factory.tsx create mode 100644 x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/index.ts create mode 100644 x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/use_data_visualizer_grid_data.ts create mode 100644 x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/index.ts create mode 100644 x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_discover.ts create mode 100644 x-pack/test/functional/apps/ml/data_visualizer/index_test_data.ts create mode 100644 x-pack/test/functional/apps/ml/data_visualizer/types.ts diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts index b30fcf972eda5..32704d95423f7 100644 --- a/src/plugins/discover/common/index.ts +++ b/src/plugins/discover/common/index.ts @@ -19,5 +19,6 @@ export const DOC_TABLE_LEGACY = 'doc_table:legacy'; export const MODIFY_COLUMNS_ON_SWITCH = 'discover:modifyColumnsOnSwitch'; export const SEARCH_FIELDS_FROM_SOURCE = 'discover:searchFieldsFromSource'; export const MAX_DOC_FIELDS_DISPLAYED = 'discover:maxDocFieldsDisplayed'; +export const SHOW_FIELD_STATISTICS = 'discover:showFieldStatistics'; export const SHOW_MULTIFIELDS = 'discover:showMultiFields'; export const SEARCH_EMBEDDABLE_TYPE = 'search'; diff --git a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx index 15f6e619c8650..f7a383be76b9e 100644 --- a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx @@ -19,6 +19,7 @@ import { discoverServiceMock } from '../../../../../__mocks__/services'; import { FetchStatus } from '../../../../types'; import { Chart } from './point_series'; import { DiscoverChart } from './discover_chart'; +import { VIEW_MODE } from '../view_mode_toggle'; setHeaderActionMenuMounter(jest.fn()); @@ -94,6 +95,8 @@ function getProps(timefield?: string) { state: { columns: [] }, stateContainer: {} as GetStateReturn, timefield, + viewMode: VIEW_MODE.DOCUMENT_LEVEL, + setDiscoverViewMode: jest.fn(), }; } diff --git a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx index b6509356c8c41..166c2272a00f4 100644 --- a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx +++ b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx @@ -23,6 +23,8 @@ import { DiscoverHistogram } from './histogram'; import { DataCharts$, DataTotalHits$ } from '../../services/use_saved_search'; import { DiscoverServices } from '../../../../../build_services'; import { useChartPanels } from './use_chart_panels'; +import { VIEW_MODE, DocumentViewModeToggle } from '../view_mode_toggle'; +import { SHOW_FIELD_STATISTICS } from '../../../../../../common'; const DiscoverHistogramMemoized = memo(DiscoverHistogram); export const CHART_HIDDEN_KEY = 'discover:chartHidden'; @@ -36,6 +38,8 @@ export function DiscoverChart({ state, stateContainer, timefield, + viewMode, + setDiscoverViewMode, }: { resetSavedSearch: () => void; savedSearch: SavedSearch; @@ -45,8 +49,11 @@ export function DiscoverChart({ state: AppState; stateContainer: GetStateReturn; timefield?: string; + viewMode: VIEW_MODE; + setDiscoverViewMode: (viewMode: VIEW_MODE) => void; }) { const [showChartOptionsPopover, setShowChartOptionsPopover] = useState(false); + const showViewModeToggle = services.uiSettings.get(SHOW_FIELD_STATISTICS) ?? false; const { data, storage } = services; @@ -108,6 +115,16 @@ export function DiscoverChart({ onResetQuery={resetSavedSearch} />
+ + {showViewModeToggle && ( + + + + )} + {timefield && ( (undefined); const [inspectorSession, setInspectorSession] = useState(undefined); + + const viewMode = useMemo(() => { + if (uiSettings.get(SHOW_FIELD_STATISTICS) !== true) return VIEW_MODE.DOCUMENT_LEVEL; + return state.viewMode ?? VIEW_MODE.DOCUMENT_LEVEL; + }, [uiSettings, state.viewMode]); + + const setDiscoverViewMode = useCallback( + (mode: VIEW_MODE) => { + stateContainer.setAppState({ viewMode: mode }); + }, + [stateContainer] + ); + const fetchCounter = useRef(0); const dataState: DataMainMsg = useDataState(main$); @@ -213,6 +229,7 @@ export function DiscoverLayout({ trackUiMetric={trackUiMetric} useNewFieldsApi={useNewFieldsApi} onEditRuntimeField={onEditRuntimeField} + viewMode={viewMode} /> @@ -279,22 +296,36 @@ export function DiscoverLayout({ services={services} stateContainer={stateContainer} timefield={timeField} + viewMode={viewMode} + setDiscoverViewMode={setDiscoverViewMode} /> - - + {viewMode === VIEW_MODE.DOCUMENT_LEVEL ? ( + + ) : ( + + )}
)} diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx index f2919f6a9bfd4..89e7b50187630 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx @@ -19,6 +19,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, + EuiHorizontalRule, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { UiCounterMetricType } from '@kbn/analytics'; @@ -251,6 +252,11 @@ export interface DiscoverFieldProps { * @param fieldName name of the field to delete */ onDeleteField?: (fieldName: string) => void; + + /** + * Optionally show or hide field stats in the popover + */ + showFieldStats?: boolean; } function DiscoverFieldComponent({ @@ -266,6 +272,7 @@ function DiscoverFieldComponent({ multiFields, onEditField, onDeleteField, + showFieldStats, }: DiscoverFieldProps) { const [infoIsOpen, setOpen] = useState(false); @@ -362,15 +369,27 @@ function DiscoverFieldComponent({ const details = getDetails(field); return ( <> - + {showFieldStats && ( + <> + +
+ {i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { + defaultMessage: 'Top 5 values', + })} +
+
+ + + )} + {multiFields && ( <> - + {showFieldStats && } )} + {(showFieldStats || multiFields) && } ); }; - return ( {popoverTitle} - -
- {i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { - defaultMessage: 'Top 5 values', - })} -
-
{infoIsOpen && renderPopover()}
); diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_visualize.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_visualize.tsx index baf740531e6bf..e974a67aef60d 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_visualize.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_visualize.tsx @@ -7,7 +7,7 @@ */ import React, { useEffect, useState } from 'react'; -import { EuiButton, EuiPopoverFooter } from '@elastic/eui'; +import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics'; import type { IndexPattern, IndexPatternField } from 'src/plugins/data/common'; @@ -46,21 +46,19 @@ export const DiscoverFieldVisualize: React.FC = React.memo( }; return ( - - {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} - - - - + // eslint-disable-next-line @elastic/eui/href-or-on-click + + + ); } ); diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.test.tsx index a550dbd59b9fa..03616c136df3e 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.test.tsx @@ -22,6 +22,7 @@ import { DiscoverSidebarComponent as DiscoverSidebar } from './discover_sidebar' import { ElasticSearchHit } from '../../../../doc_views/doc_views_types'; import { discoverServiceMock as mockDiscoverServices } from '../../../../../__mocks__/services'; import { stubLogstashIndexPattern } from '../../../../../../../data/common/stubs'; +import { VIEW_MODE } from '../view_mode_toggle'; jest.mock('../../../../../kibana_services', () => ({ getServices: () => mockDiscoverServices, @@ -65,6 +66,7 @@ function getCompProps(): DiscoverSidebarProps { setFieldFilter: jest.fn(), onEditRuntimeField: jest.fn(), editField: jest.fn(), + viewMode: VIEW_MODE.DOCUMENT_LEVEL, }; } diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx index 0bd8c59b90c01..d13860eab0d24 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx @@ -40,6 +40,7 @@ import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list'; import { DiscoverSidebarResponsiveProps } from './discover_sidebar_responsive'; import { DiscoverIndexPatternManagement } from './discover_index_pattern_management'; import { ElasticSearchHit } from '../../../../doc_views/doc_views_types'; +import { VIEW_MODE } from '../view_mode_toggle'; /** * Default number of available fields displayed and added on scroll @@ -77,6 +78,10 @@ export interface DiscoverSidebarProps extends Omit(null); @@ -205,6 +211,8 @@ export function DiscoverSidebarComponent({ return result; }, [fields]); + const showFieldStats = useMemo(() => viewMode === VIEW_MODE.DOCUMENT_LEVEL, [viewMode]); + const calculateMultiFields = () => { if (!useNewFieldsApi || !fields) { return undefined; @@ -407,6 +415,7 @@ export function DiscoverSidebarComponent({ multiFields={multiFields?.get(field.name)} onEditField={canEditIndexPatternField ? editField : undefined} onDeleteField={canEditIndexPatternField ? deleteField : undefined} + showFieldStats={showFieldStats} /> ); @@ -466,6 +475,7 @@ export function DiscoverSidebarComponent({ multiFields={multiFields?.get(field.name)} onEditField={canEditIndexPatternField ? editField : undefined} onDeleteField={canEditIndexPatternField ? deleteField : undefined} + showFieldStats={showFieldStats} /> ); @@ -494,6 +504,7 @@ export function DiscoverSidebarComponent({ multiFields={multiFields?.get(field.name)} onEditField={canEditIndexPatternField ? editField : undefined} onDeleteField={canEditIndexPatternField ? deleteField : undefined} + showFieldStats={showFieldStats} /> ); diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx index ded7897d2a9e5..4e4fed8c65bf7 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx @@ -26,6 +26,7 @@ import { ElasticSearchHit } from '../../../../doc_views/doc_views_types'; import { FetchStatus } from '../../../../types'; import { DataDocuments$ } from '../../services/use_saved_search'; import { stubLogstashIndexPattern } from '../../../../../../../data/common/stubs'; +import { VIEW_MODE } from '../view_mode_toggle'; const mockServices = { history: () => ({ @@ -103,6 +104,7 @@ function getCompProps(): DiscoverSidebarResponsiveProps { state: {}, trackUiMetric: jest.fn(), onEditRuntimeField: jest.fn(), + viewMode: VIEW_MODE.DOCUMENT_LEVEL, }; } diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx index 90357b73c6881..368a2b2e92d34 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx @@ -37,6 +37,7 @@ import { AppState } from '../../services/discover_state'; import { DiscoverIndexPatternManagement } from './discover_index_pattern_management'; import { DataDocuments$ } from '../../services/use_saved_search'; import { calcFieldCounts } from '../../utils/calc_field_counts'; +import { VIEW_MODE } from '../view_mode_toggle'; export interface DiscoverSidebarResponsiveProps { /** @@ -106,6 +107,10 @@ export interface DiscoverSidebarResponsiveProps { * callback to execute on edit runtime field */ onEditRuntimeField: () => void; + /** + * Discover view mode + */ + viewMode: VIEW_MODE; } /** diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts index 44d2999947f41..653e878ad01bb 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts @@ -16,6 +16,7 @@ import { SavedSearch } from '../../../../../saved_searches'; import { onSaveSearch } from './on_save_search'; import { GetStateReturn } from '../../services/discover_state'; import { openOptionsPopover } from './open_options_popover'; +import type { TopNavMenuData } from '../../../../../../../navigation/public'; /** * Helper function to build the top nav links @@ -38,7 +39,7 @@ export const getTopNavLinks = ({ onOpenInspector: () => void; searchSource: ISearchSource; onOpenSavedSearch: (id: string) => void; -}) => { +}): TopNavMenuData[] => { const options = { id: 'options', label: i18n.translate('discover.localMenu.localMenu.optionsTitle', { diff --git a/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/_index.scss b/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/_index.scss new file mode 100644 index 0000000000000..a76c3453de32a --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/_index.scss @@ -0,0 +1 @@ +@import 'view_mode_toggle'; diff --git a/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/_view_mode_toggle.scss b/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/_view_mode_toggle.scss new file mode 100644 index 0000000000000..1009ab0511957 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/_view_mode_toggle.scss @@ -0,0 +1,12 @@ +.dscViewModeToggle { + padding-right: $euiSize; +} + +.fieldStatsButton { + display: flex; + align-items: center; +} + +.fieldStatsBetaBadge { + margin-left: $euiSizeXS; +} diff --git a/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/constants.ts b/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/constants.ts new file mode 100644 index 0000000000000..d03c0710d12b3 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/constants.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export enum VIEW_MODE { + DOCUMENT_LEVEL = 'documents', + AGGREGATED_LEVEL = 'aggregated', +} diff --git a/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/index.ts b/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/index.ts new file mode 100644 index 0000000000000..95b76f5879d19 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { DocumentViewModeToggle } from './view_mode_toggle'; +export { VIEW_MODE } from './constants'; diff --git a/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/view_mode_toggle.tsx b/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/view_mode_toggle.tsx new file mode 100644 index 0000000000000..3aa24c05e98d4 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/view_mode_toggle.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { EuiButtonGroup, EuiBetaBadge } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { VIEW_MODE } from './constants'; +import './_index.scss'; + +export const DocumentViewModeToggle = ({ + viewMode, + setDiscoverViewMode, +}: { + viewMode: VIEW_MODE; + setDiscoverViewMode: (viewMode: VIEW_MODE) => void; +}) => { + const toggleButtons = useMemo( + () => [ + { + id: VIEW_MODE.DOCUMENT_LEVEL, + label: i18n.translate('discover.viewModes.document.label', { + defaultMessage: 'Documents', + }), + 'data-test-subj': 'dscViewModeDocumentButton', + }, + { + id: VIEW_MODE.AGGREGATED_LEVEL, + label: ( +
+ + +
+ ), + }, + ], + [] + ); + + return ( + setDiscoverViewMode(id as VIEW_MODE)} + data-test-subj={'dscViewModeToggle'} + /> + ); +}; diff --git a/src/plugins/discover/public/application/apps/main/services/discover_state.ts b/src/plugins/discover/public/application/apps/main/services/discover_state.ts index 16eb622c4a7c4..9a61fdc996e3b 100644 --- a/src/plugins/discover/public/application/apps/main/services/discover_state.ts +++ b/src/plugins/discover/public/application/apps/main/services/discover_state.ts @@ -35,6 +35,7 @@ import { DiscoverGridSettings } from '../../../components/discover_grid/types'; import { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from '../../../../url_generator'; import { SavedSearch } from '../../../../saved_searches'; import { handleSourceColumnState } from '../../../helpers/state_helpers'; +import { VIEW_MODE } from '../components/view_mode_toggle'; export interface AppState { /** @@ -73,6 +74,14 @@ export interface AppState { * id of the used saved query */ savedQuery?: string; + /** + * Table view: Documents vs Field Statistics + */ + viewMode?: VIEW_MODE; + /** + * Hide mini distribution/preview charts when in Field Statistics mode + */ + hideAggregatedPreview?: boolean; } interface GetStateParams { diff --git a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts index 45447fe642ad4..6cf34fd8cb024 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts @@ -31,6 +31,7 @@ describe('getStateDefaults', () => { "default_column", ], "filters": undefined, + "hideAggregatedPreview": undefined, "hideChart": undefined, "index": "index-pattern-with-timefield-id", "interval": "auto", @@ -42,6 +43,7 @@ describe('getStateDefaults', () => { "desc", ], ], + "viewMode": undefined, } `); }); @@ -61,12 +63,14 @@ describe('getStateDefaults', () => { "default_column", ], "filters": undefined, + "hideAggregatedPreview": undefined, "hideChart": undefined, "index": "the-index-pattern-id", "interval": "auto", "query": undefined, "savedQuery": undefined, "sort": Array [], + "viewMode": undefined, } `); }); diff --git a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts index 6fa4dda2eab19..50dab0273d461 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts @@ -60,6 +60,8 @@ export function getStateDefaults({ interval: 'auto', filters: cloneDeep(searchSource.getOwnField('filter')), hideChart: chartHidden ? chartHidden : undefined, + viewMode: undefined, + hideAggregatedPreview: undefined, savedQuery: undefined, } as AppState; if (savedSearch.grid) { @@ -68,6 +70,13 @@ export function getStateDefaults({ if (savedSearch.hideChart !== undefined) { defaultState.hideChart = savedSearch.hideChart; } + if (savedSearch.viewMode) { + defaultState.viewMode = savedSearch.viewMode; + } + + if (savedSearch.hideAggregatedPreview) { + defaultState.hideAggregatedPreview = savedSearch.hideAggregatedPreview; + } return defaultState; } diff --git a/src/plugins/discover/public/application/apps/main/utils/persist_saved_search.ts b/src/plugins/discover/public/application/apps/main/utils/persist_saved_search.ts index 584fbe14cb59e..fa566fd485942 100644 --- a/src/plugins/discover/public/application/apps/main/utils/persist_saved_search.ts +++ b/src/plugins/discover/public/application/apps/main/utils/persist_saved_search.ts @@ -52,6 +52,14 @@ export async function persistSavedSearch( savedSearch.hideChart = state.hideChart; } + if (state.viewMode) { + savedSearch.viewMode = state.viewMode; + } + + if (state.hideAggregatedPreview) { + savedSearch.hideAggregatedPreview = state.hideAggregatedPreview; + } + try { const id = await saveSavedSearch(savedSearch, saveOptions, services.core.savedObjects.client); if (id) { diff --git a/src/plugins/discover/public/application/components/data_visualizer_grid/data_visualizer_grid.tsx b/src/plugins/discover/public/application/components/data_visualizer_grid/data_visualizer_grid.tsx new file mode 100644 index 0000000000000..5492fac014b74 --- /dev/null +++ b/src/plugins/discover/public/application/components/data_visualizer_grid/data_visualizer_grid.tsx @@ -0,0 +1,196 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { Filter } from '@kbn/es-query'; +import { IndexPatternField, IndexPattern, DataView, Query } from '../../../../../data/common'; +import { DiscoverServices } from '../../../build_services'; +import { + EmbeddableInput, + EmbeddableOutput, + ErrorEmbeddable, + IEmbeddable, + isErrorEmbeddable, +} from '../../../../../embeddable/public'; +import { SavedSearch } from '../../../saved_searches'; +import { GetStateReturn } from '../../apps/main/services/discover_state'; + +export interface DataVisualizerGridEmbeddableInput extends EmbeddableInput { + indexPattern: IndexPattern; + savedSearch?: SavedSearch; + query?: Query; + visibleFieldNames?: string[]; + filters?: Filter[]; + showPreviewByDefault?: boolean; + /** + * Callback to add a filter to filter bar + */ + onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; +} +export interface DataVisualizerGridEmbeddableOutput extends EmbeddableOutput { + showDistributions?: boolean; +} + +export interface DiscoverDataVisualizerGridProps { + /** + * Determines which columns are displayed + */ + columns: string[]; + /** + * The used index pattern + */ + indexPattern: DataView; + /** + * Saved search description + */ + searchDescription?: string; + /** + * Saved search title + */ + searchTitle?: string; + /** + * Discover plugin services + */ + services: DiscoverServices; + /** + * Optional saved search + */ + savedSearch?: SavedSearch; + /** + * Optional query to update the table content + */ + query?: Query; + /** + * Filters query to update the table content + */ + filters?: Filter[]; + stateContainer?: GetStateReturn; + /** + * Callback to add a filter to filter bar + */ + onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; +} + +export const DiscoverDataVisualizerGrid = (props: DiscoverDataVisualizerGridProps) => { + const { + services, + indexPattern, + savedSearch, + query, + columns, + filters, + stateContainer, + onAddFilter, + } = props; + const { uiSettings } = services; + + const [embeddable, setEmbeddable] = useState< + | ErrorEmbeddable + | IEmbeddable + | undefined + >(); + const embeddableRoot: React.RefObject = useRef(null); + + const showPreviewByDefault = useMemo( + () => + stateContainer ? !stateContainer.appStateContainer.getState().hideAggregatedPreview : true, + [stateContainer] + ); + + useEffect(() => { + const sub = embeddable?.getOutput$().subscribe((output: DataVisualizerGridEmbeddableOutput) => { + if (output.showDistributions !== undefined && stateContainer) { + stateContainer.setAppState({ hideAggregatedPreview: !output.showDistributions }); + } + }); + + return () => { + sub?.unsubscribe(); + }; + }, [embeddable, stateContainer]); + + useEffect(() => { + if (embeddable && !isErrorEmbeddable(embeddable)) { + // Update embeddable whenever one of the important input changes + embeddable.updateInput({ + indexPattern, + savedSearch, + query, + filters, + visibleFieldNames: columns, + onAddFilter, + }); + embeddable.reload(); + } + }, [embeddable, indexPattern, savedSearch, query, columns, filters, onAddFilter]); + + useEffect(() => { + if (showPreviewByDefault && embeddable && !isErrorEmbeddable(embeddable)) { + // Update embeddable whenever one of the important input changes + embeddable.updateInput({ + showPreviewByDefault, + }); + embeddable.reload(); + } + }, [showPreviewByDefault, uiSettings, embeddable]); + + useEffect(() => { + return () => { + // Clean up embeddable upon unmounting + embeddable?.destroy(); + }; + }, [embeddable]); + + useEffect(() => { + let unmounted = false; + const loadEmbeddable = async () => { + if (services.embeddable) { + const factory = services.embeddable.getEmbeddableFactory< + DataVisualizerGridEmbeddableInput, + DataVisualizerGridEmbeddableOutput + >('data_visualizer_grid'); + if (factory) { + // Initialize embeddable with information available at mount + const initializedEmbeddable = await factory.create({ + id: 'discover_data_visualizer_grid', + indexPattern, + savedSearch, + query, + showPreviewByDefault, + onAddFilter, + }); + if (!unmounted) { + setEmbeddable(initializedEmbeddable); + } + } + } + }; + loadEmbeddable(); + return () => { + unmounted = true; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [services.embeddable, showPreviewByDefault]); + + // We can only render after embeddable has already initialized + useEffect(() => { + if (embeddableRoot.current && embeddable) { + embeddable.render(embeddableRoot.current); + } + }, [embeddable, embeddableRoot, uiSettings]); + + return ( +
+ ); +}; diff --git a/src/plugins/discover/public/application/components/data_visualizer_grid/field_stats_table_embeddable.tsx b/src/plugins/discover/public/application/components/data_visualizer_grid/field_stats_table_embeddable.tsx new file mode 100644 index 0000000000000..099f45bf988cc --- /dev/null +++ b/src/plugins/discover/public/application/components/data_visualizer_grid/field_stats_table_embeddable.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { I18nProvider } from '@kbn/i18n/react'; +import { + DiscoverDataVisualizerGrid, + DiscoverDataVisualizerGridProps, +} from './data_visualizer_grid'; + +export function FieldStatsTableEmbeddable(renderProps: DiscoverDataVisualizerGridProps) { + return ( + + + + ); +} diff --git a/src/plugins/discover/public/application/components/data_visualizer_grid/index.ts b/src/plugins/discover/public/application/components/data_visualizer_grid/index.ts new file mode 100644 index 0000000000000..dc85495a7c2ec --- /dev/null +++ b/src/plugins/discover/public/application/components/data_visualizer_grid/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { DiscoverDataVisualizerGrid } from './data_visualizer_grid'; diff --git a/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx index 89c47559d7b4c..808962dc8319d 100644 --- a/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx @@ -19,12 +19,12 @@ import { SEARCH_EMBEDDABLE_TYPE } from './constants'; import { APPLY_FILTER_TRIGGER, esFilters, FilterManager } from '../../../../data/public'; import { DiscoverServices } from '../../build_services'; import { - Query, - TimeRange, Filter, IndexPattern, - ISearchSource, IndexPatternField, + ISearchSource, + Query, + TimeRange, } from '../../../../data/common'; import { ElasticSearchHit } from '../doc_views/doc_views_types'; import { SavedSearchEmbeddableComponent } from './saved_search_embeddable_component'; @@ -35,6 +35,7 @@ import { DOC_TABLE_LEGACY, SAMPLE_SIZE_SETTING, SEARCH_FIELDS_FROM_SOURCE, + SHOW_FIELD_STATISTICS, SORT_DEFAULT_ORDER_SETTING, } from '../../../common'; import * as columnActions from '../apps/main/components/doc_table/actions/columns'; @@ -45,6 +46,8 @@ import { DocTableProps } from '../apps/main/components/doc_table/doc_table_wrapp import { getDefaultSort } from '../apps/main/components/doc_table'; import { SortOrder } from '../apps/main/components/doc_table/components/table_header/helpers'; import { updateSearchSource } from './helpers/update_search_source'; +import { VIEW_MODE } from '../apps/main/components/view_mode_toggle'; +import { FieldStatsTableEmbeddable } from '../components/data_visualizer_grid/field_stats_table_embeddable'; export type SearchProps = Partial & Partial & { @@ -379,6 +382,28 @@ export class SavedSearchEmbeddable if (!this.searchProps) { return; } + + if ( + this.services.uiSettings.get(SHOW_FIELD_STATISTICS) === true && + this.savedSearch.viewMode === VIEW_MODE.AGGREGATED_LEVEL && + searchProps.services && + searchProps.indexPattern && + Array.isArray(searchProps.columns) + ) { + ReactDOM.render( + , + domNode + ); + return; + } const useLegacyTable = this.services.uiSettings.get(DOC_TABLE_LEGACY); const props = { searchProps, diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index ac16b6b3cc2ba..a6b175e34bd13 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -37,6 +37,7 @@ import { UrlForwardingStart } from '../../url_forwarding/public'; import { NavigationPublicPluginStart } from '../../navigation/public'; import { IndexPatternFieldEditorStart } from '../../index_pattern_field_editor/public'; import { FieldFormatsStart } from '../../field_formats/public'; +import { EmbeddableStart } from '../../embeddable/public'; import type { SpacesApi } from '../../../../x-pack/plugins/spaces/public'; @@ -47,6 +48,7 @@ export interface DiscoverServices { core: CoreStart; data: DataPublicPluginStart; docLinks: DocLinksStart; + embeddable: EmbeddableStart; history: () => History; theme: ChartsPluginStart['theme']; filterManager: FilterManager; @@ -83,6 +85,7 @@ export function buildServices( core, data: plugins.data, docLinks: core.docLinks, + embeddable: plugins.embeddable, theme: plugins.charts.theme, fieldFormats: plugins.fieldFormats, filterManager: plugins.data.query.filterManager, diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index e170e61f7ebc5..c91bcf3897e14 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -348,6 +348,11 @@ export class DiscoverPlugin await depsStart.data.indexPatterns.clearCache(); const { renderApp } = await import('./application'); + + // FIXME: Temporarily hide overflow-y in Discover app when Field Stats table is shown + // due to EUI bug https://github.com/elastic/eui/pull/5152 + params.element.classList.add('dscAppWrapper'); + const unmount = renderApp(params.element); return () => { unlistenParentHistory(); diff --git a/src/plugins/discover/public/saved_searches/get_saved_searches.test.ts b/src/plugins/discover/public/saved_searches/get_saved_searches.test.ts index 755831e7009ed..560e16b12e5ed 100644 --- a/src/plugins/discover/public/saved_searches/get_saved_searches.test.ts +++ b/src/plugins/discover/public/saved_searches/get_saved_searches.test.ts @@ -101,6 +101,7 @@ describe('getSavedSearch', () => { ], "description": "description", "grid": Object {}, + "hideAggregatedPreview": undefined, "hideChart": false, "id": "ccf1af80-2297-11ec-86e0-1155ffb9c7a7", "searchSource": Object { @@ -138,6 +139,7 @@ describe('getSavedSearch', () => { ], ], "title": "test1", + "viewMode": undefined, } `); }); diff --git a/src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts b/src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts index 12c73e86b3dc4..82510340f30f1 100644 --- a/src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts +++ b/src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts @@ -54,6 +54,7 @@ describe('saved_searches_utils', () => { ], "description": "foo", "grid": Object {}, + "hideAggregatedPreview": undefined, "hideChart": true, "id": "id", "searchSource": SearchSource { @@ -74,6 +75,7 @@ describe('saved_searches_utils', () => { "sharingSavedObjectProps": Object {}, "sort": Array [], "title": "saved search", + "viewMode": undefined, } `); }); @@ -122,6 +124,7 @@ describe('saved_searches_utils', () => { ], "description": "description", "grid": Object {}, + "hideAggregatedPreview": undefined, "hideChart": true, "kibanaSavedObjectMeta": Object { "searchSourceJSON": "{}", @@ -133,6 +136,7 @@ describe('saved_searches_utils', () => { ], ], "title": "title", + "viewMode": undefined, } `); }); diff --git a/src/plugins/discover/public/saved_searches/saved_searches_utils.ts b/src/plugins/discover/public/saved_searches/saved_searches_utils.ts index 98ab2267a875e..064ee6afe0e99 100644 --- a/src/plugins/discover/public/saved_searches/saved_searches_utils.ts +++ b/src/plugins/discover/public/saved_searches/saved_searches_utils.ts @@ -41,6 +41,8 @@ export const fromSavedSearchAttributes = ( description: attributes.description, grid: attributes.grid, hideChart: attributes.hideChart, + viewMode: attributes.viewMode, + hideAggregatedPreview: attributes.hideAggregatedPreview, }); export const toSavedSearchAttributes = ( @@ -54,4 +56,6 @@ export const toSavedSearchAttributes = ( description: savedSearch.description ?? '', grid: savedSearch.grid ?? {}, hideChart: savedSearch.hideChart ?? false, + viewMode: savedSearch.viewMode, + hideAggregatedPreview: savedSearch.hideAggregatedPreview, }); diff --git a/src/plugins/discover/public/saved_searches/types.ts b/src/plugins/discover/public/saved_searches/types.ts index 10a6282063d38..b3a67ea57e769 100644 --- a/src/plugins/discover/public/saved_searches/types.ts +++ b/src/plugins/discover/public/saved_searches/types.ts @@ -8,6 +8,7 @@ import type { ISearchSource } from '../../../data/public'; import { DiscoverGridSettingsColumn } from '../application/components/discover_grid/types'; +import { VIEW_MODE } from '../application/apps/main/components/view_mode_toggle'; /** @internal **/ export interface SavedSearchAttributes { @@ -22,6 +23,8 @@ export interface SavedSearchAttributes { kibanaSavedObjectMeta: { searchSourceJSON: string; }; + viewMode?: VIEW_MODE; + hideAggregatedPreview?: boolean; } /** @internal **/ @@ -44,4 +47,6 @@ export interface SavedSearch { aliasTargetId?: string; errorJSON?: string; }; + viewMode?: VIEW_MODE; + hideAggregatedPreview?: boolean; } diff --git a/src/plugins/discover/server/saved_objects/search.ts b/src/plugins/discover/server/saved_objects/search.ts index 6a85685407612..23d9312e82897 100644 --- a/src/plugins/discover/server/saved_objects/search.ts +++ b/src/plugins/discover/server/saved_objects/search.ts @@ -32,7 +32,9 @@ export const searchSavedObjectType: SavedObjectsType = { properties: { columns: { type: 'keyword', index: false, doc_values: false }, description: { type: 'text' }, + viewMode: { type: 'keyword', index: false, doc_values: false }, hideChart: { type: 'boolean', index: false, doc_values: false }, + hideAggregatedPreview: { type: 'boolean', index: false, doc_values: false }, hits: { type: 'integer', index: false, doc_values: false }, kibanaSavedObjectMeta: { properties: { diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts index d6a105bdb6263..529ba0d1beef1 100644 --- a/src/plugins/discover/server/ui_settings.ts +++ b/src/plugins/discover/server/ui_settings.ts @@ -26,6 +26,7 @@ import { SEARCH_FIELDS_FROM_SOURCE, MAX_DOC_FIELDS_DISPLAYED, SHOW_MULTIFIELDS, + SHOW_FIELD_STATISTICS, } from '../common'; export const getUiSettings: () => Record = () => ({ @@ -172,6 +173,7 @@ export const getUiSettings: () => Record = () => ({ name: 'discover:useLegacyDataGrid', }, }, + [MODIFY_COLUMNS_ON_SWITCH]: { name: i18n.translate('discover.advancedSettings.discover.modifyColumnsOnSwitchTitle', { defaultMessage: 'Modify columns when changing data views', @@ -201,6 +203,24 @@ export const getUiSettings: () => Record = () => ({ category: ['discover'], schema: schema.boolean(), }, + [SHOW_FIELD_STATISTICS]: { + name: i18n.translate('discover.advancedSettings.discover.showFieldStatistics', { + defaultMessage: 'Show field statistics', + }), + description: i18n.translate( + 'discover.advancedSettings.discover.showFieldStatisticsDescription', + { + defaultMessage: `Enable "Field statistics" table in Discover.`, + } + ), + value: false, + category: ['discover'], + schema: schema.boolean(), + metric: { + type: METRIC_TYPE.CLICK, + name: 'discover:showFieldStatistics', + }, + }, [SHOW_MULTIFIELDS]: { name: i18n.translate('discover.advancedSettings.discover.showMultifields', { defaultMessage: 'Show multi-fields', diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index a8a391995b005..bf936b2ae8dbe 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -448,6 +448,10 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'discover:showFieldStatistics': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, 'discover:showMultiFields': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 7ea80ffb77dda..7575fa5d2b3f3 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -31,6 +31,7 @@ export interface UsageStats { 'doc_table:legacy': boolean; 'discover:modifyColumnsOnSwitch': boolean; 'discover:searchFieldsFromSource': boolean; + 'discover:showFieldStatistics': boolean; 'discover:showMultiFields': boolean; 'discover:maxDocFieldsDisplayed': number; 'securitySolution:rulesTableRefresh': string; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index c6724056f77a5..f9ca99a26ec19 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -7689,6 +7689,12 @@ "description": "Non-default value of setting." } }, + "discover:showFieldStatistics": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } + }, "discover:showMultiFields": { "type": "boolean", "_meta": { diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index a6ee65e0febb5..a45c1a23ed3a5 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import expect from '@kbn/expect'; import { FtrService } from '../ftr_provider_context'; export class DiscoverPageObject extends FtrService { @@ -307,6 +308,13 @@ export class DiscoverPageObject extends FtrService { return await this.testSubjects.click('collapseSideBarButton'); } + public async closeSidebar() { + await this.retry.tryForTime(2 * 1000, async () => { + await this.toggleSidebarCollapse(); + await this.testSubjects.missingOrFail('discover-sidebar'); + }); + } + public async getAllFieldNames() { const sidebar = await this.testSubjects.find('discover-sidebar'); const $ = await sidebar.parseDomContent(); @@ -545,4 +553,37 @@ export class DiscoverPageObject extends FtrService { public async clearSavedQuery() { await this.testSubjects.click('saved-query-management-clear-button'); } + + public async assertHitCount(expectedHitCount: string) { + await this.retry.tryForTime(2 * 1000, async () => { + // Close side bar to ensure Discover hit count shows + // edge case for when browser width is small + await this.closeSidebar(); + const hitCount = await this.getHitCount(); + expect(hitCount).to.eql( + expectedHitCount, + `Expected Discover hit count to be ${expectedHitCount} but got ${hitCount}.` + ); + }); + } + + public async assertViewModeToggleNotExists() { + await this.testSubjects.missingOrFail('dscViewModeToggle', { timeout: 2 * 1000 }); + } + + public async assertViewModeToggleExists() { + await this.testSubjects.existOrFail('dscViewModeToggle', { timeout: 2 * 1000 }); + } + + public async assertFieldStatsTableNotExists() { + await this.testSubjects.missingOrFail('dscFieldStatsEmbeddedContent', { timeout: 2 * 1000 }); + } + + public async clickViewModeFieldStatsButton() { + await this.retry.tryForTime(2 * 1000, async () => { + await this.testSubjects.existOrFail('dscViewModeFieldStatsButton'); + await this.testSubjects.clickWhenNotDisabled('dscViewModeFieldStatsButton'); + await this.testSubjects.existOrFail('dscFieldStatsEmbeddedContent'); + }); + } } diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap index 927d8ddb7a851..398dc5dad2dc7 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap +++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap @@ -10,7 +10,7 @@ exports[`FieldTypeIcon render component when type matches a field type 1`] = ` > diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx index 2373cfe1f3284..9d803e3d4a80c 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx @@ -48,7 +48,7 @@ export const typeToEuiIconMap: Record { diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/constants.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/constants.ts new file mode 100644 index 0000000000000..26004db8fd529 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/constants.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE = 'data_visualizer_grid'; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/embeddable_loading_fallback.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/embeddable_loading_fallback.tsx new file mode 100644 index 0000000000000..01644efd6652c --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/embeddable_loading_fallback.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui'; + +export const EmbeddableLoading = () => { + return ( + + + + + + ); +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable.tsx new file mode 100644 index 0000000000000..f59225b1c019f --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable.tsx @@ -0,0 +1,234 @@ +/* + * 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 { Observable, Subject } from 'rxjs'; +import { CoreStart } from 'kibana/public'; +import ReactDOM from 'react-dom'; +import React, { Suspense, useCallback, useState } from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { EuiEmptyPrompt, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; +import { Filter } from '@kbn/es-query'; +import { Required } from 'utility-types'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + Embeddable, + EmbeddableInput, + EmbeddableOutput, + IContainer, +} from '../../../../../../../../src/plugins/embeddable/public'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; +import { DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE } from './constants'; +import { EmbeddableLoading } from './embeddable_loading_fallback'; +import { DataVisualizerStartDependencies } from '../../../../plugin'; +import { + IndexPattern, + IndexPatternField, + Query, +} from '../../../../../../../../src/plugins/data/common'; +import { SavedSearch } from '../../../../../../../../src/plugins/discover/public'; +import { + DataVisualizerTable, + ItemIdToExpandedRowMap, +} from '../../../common/components/stats_table'; +import { FieldVisConfig } from '../../../common/components/stats_table/types'; +import { getDefaultDataVisualizerListState } from '../../components/index_data_visualizer_view/index_data_visualizer_view'; +import { DataVisualizerTableState } from '../../../../../common'; +import { DataVisualizerIndexBasedAppState } from '../../types/index_data_visualizer_state'; +import { IndexBasedDataVisualizerExpandedRow } from '../../../common/components/expanded_row/index_based_expanded_row'; +import { useDataVisualizerGridData } from './use_data_visualizer_grid_data'; + +export type DataVisualizerGridEmbeddableServices = [CoreStart, DataVisualizerStartDependencies]; +export interface DataVisualizerGridEmbeddableInput extends EmbeddableInput { + indexPattern: IndexPattern; + savedSearch?: SavedSearch; + query?: Query; + visibleFieldNames?: string[]; + filters?: Filter[]; + showPreviewByDefault?: boolean; + /** + * Callback to add a filter to filter bar + */ + onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; +} +export type DataVisualizerGridEmbeddableOutput = EmbeddableOutput; + +export type IDataVisualizerGridEmbeddable = typeof DataVisualizerGridEmbeddable; + +const restorableDefaults = getDefaultDataVisualizerListState(); + +export const EmbeddableWrapper = ({ + input, + onOutputChange, +}: { + input: DataVisualizerGridEmbeddableInput; + onOutputChange?: (ouput: any) => void; +}) => { + const [dataVisualizerListState, setDataVisualizerListState] = + useState>(restorableDefaults); + + const onTableChange = useCallback( + (update: DataVisualizerTableState) => { + setDataVisualizerListState({ ...dataVisualizerListState, ...update }); + if (onOutputChange) { + onOutputChange(update); + } + }, + [dataVisualizerListState, onOutputChange] + ); + const { configs, searchQueryLanguage, searchString, extendedColumns, loaded } = + useDataVisualizerGridData(input, dataVisualizerListState); + const getItemIdToExpandedRowMap = useCallback( + function (itemIds: string[], items: FieldVisConfig[]): ItemIdToExpandedRowMap { + return itemIds.reduce((m: ItemIdToExpandedRowMap, fieldName: string) => { + const item = items.find((fieldVisConfig) => fieldVisConfig.fieldName === fieldName); + if (item !== undefined) { + m[fieldName] = ( + + ); + } + return m; + }, {} as ItemIdToExpandedRowMap); + }, + [input, searchQueryLanguage, searchString] + ); + + if ( + loaded && + (configs.length === 0 || + // FIXME: Configs might have a placeholder document count stats field + // This will be removed in the future + (configs.length === 1 && configs[0].fieldName === undefined)) + ) { + return ( +
+ + + + + +
+ ); + } + return ( + + items={configs} + pageState={dataVisualizerListState} + updatePageState={onTableChange} + getItemIdToExpandedRowMap={getItemIdToExpandedRowMap} + extendedColumns={extendedColumns} + showPreviewByDefault={input?.showPreviewByDefault} + onChange={onOutputChange} + /> + ); +}; + +export const IndexDataVisualizerViewWrapper = (props: { + id: string; + embeddableContext: InstanceType; + embeddableInput: Readonly>; + onOutputChange?: (output: any) => void; +}) => { + const { embeddableInput, onOutputChange } = props; + + const input = useObservable(embeddableInput); + if (input && input.indexPattern) { + return ; + } else { + return ( + + + + } + body={ +

+ +

+ } + /> + ); + } +}; +export class DataVisualizerGridEmbeddable extends Embeddable< + DataVisualizerGridEmbeddableInput, + DataVisualizerGridEmbeddableOutput +> { + private node?: HTMLElement; + private reload$ = new Subject(); + public readonly type: string = DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE; + + constructor( + initialInput: DataVisualizerGridEmbeddableInput, + public services: DataVisualizerGridEmbeddableServices, + parent?: IContainer + ) { + super(initialInput, {}, parent); + } + + public render(node: HTMLElement) { + super.render(node); + this.node = node; + + const I18nContext = this.services[0].i18n.Context; + + ReactDOM.render( + + + }> + this.updateOutput(output)} + /> + + + , + node + ); + } + + public destroy() { + super.destroy(); + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + } + + public reload() { + this.reload$.next(); + } + + public supportedTriggers() { + return []; + } +} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable_factory.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable_factory.tsx new file mode 100644 index 0000000000000..08ddc2d5fe3c2 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable_factory.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { StartServicesAccessor } from 'kibana/public'; +import { + EmbeddableFactoryDefinition, + IContainer, +} from '../../../../../../../../src/plugins/embeddable/public'; +import { DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE } from './constants'; +import { + DataVisualizerGridEmbeddableInput, + DataVisualizerGridEmbeddableServices, +} from './grid_embeddable'; +import { DataVisualizerPluginStart, DataVisualizerStartDependencies } from '../../../../plugin'; + +export class DataVisualizerGridEmbeddableFactory + implements EmbeddableFactoryDefinition +{ + public readonly type = DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE; + + public readonly grouping = [ + { + id: 'data_visualizer_grid', + getDisplayName: () => 'Data Visualizer Grid', + }, + ]; + + constructor( + private getStartServices: StartServicesAccessor< + DataVisualizerStartDependencies, + DataVisualizerPluginStart + > + ) {} + + public async isEditable() { + return false; + } + + public canCreateNew() { + return false; + } + + public getDisplayName() { + return i18n.translate('xpack.dataVisualizer.index.components.grid.displayName', { + defaultMessage: 'Data visualizer grid', + }); + } + + public getDescription() { + return i18n.translate('xpack.dataVisualizer.index.components.grid.description', { + defaultMessage: 'Visualize data', + }); + } + + private async getServices(): Promise { + const [coreStart, pluginsStart] = await this.getStartServices(); + return [coreStart, pluginsStart]; + } + + public async create(initialInput: DataVisualizerGridEmbeddableInput, parent?: IContainer) { + const [coreStart, pluginsStart] = await this.getServices(); + const { DataVisualizerGridEmbeddable } = await import('./grid_embeddable'); + return new DataVisualizerGridEmbeddable(initialInput, [coreStart, pluginsStart], parent); + } +} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/index.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/index.ts new file mode 100644 index 0000000000000..91ca8e1633eb9 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export { DataVisualizerGridEmbeddable } from './grid_embeddable'; +export { DataVisualizerGridEmbeddableFactory } from './grid_embeddable_factory'; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/use_data_visualizer_grid_data.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/use_data_visualizer_grid_data.ts new file mode 100644 index 0000000000000..fc0fc7a2134b4 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/use_data_visualizer_grid_data.ts @@ -0,0 +1,587 @@ +/* + * 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 { Required } from 'utility-types'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { merge } from 'rxjs'; +import { EuiTableActionsColumnType } from '@elastic/eui/src/components/basic_table/table_types'; +import { i18n } from '@kbn/i18n'; +import { DataVisualizerIndexBasedAppState } from '../../types/index_data_visualizer_state'; +import { useDataVisualizerKibana } from '../../../kibana_context'; +import { getEsQueryFromSavedSearch } from '../../utils/saved_search_utils'; +import { MetricFieldsStats } from '../../../common/components/stats_table/components/field_count_stats'; +import { DataLoader } from '../../data_loader/data_loader'; +import { useTimefilter } from '../../hooks/use_time_filter'; +import { dataVisualizerRefresh$ } from '../../services/timefilter_refresh_service'; +import { TimeBuckets } from '../../services/time_buckets'; +import { + DataViewField, + KBN_FIELD_TYPES, + UI_SETTINGS, +} from '../../../../../../../../src/plugins/data/common'; +import { extractErrorProperties } from '../../utils/error_utils'; +import { FieldVisConfig } from '../../../common/components/stats_table/types'; +import { FieldRequestConfig, JOB_FIELD_TYPES } from '../../../../../common'; +import { kbnTypeToJobType } from '../../../common/util/field_types_utils'; +import { getActions } from '../../../common/components/field_data_row/action_menu'; +import { DataVisualizerGridEmbeddableInput } from './grid_embeddable'; +import { getDefaultPageState } from '../../components/index_data_visualizer_view/index_data_visualizer_view'; + +const defaults = getDefaultPageState(); + +export const useDataVisualizerGridData = ( + input: DataVisualizerGridEmbeddableInput, + dataVisualizerListState: Required +) => { + const { services } = useDataVisualizerKibana(); + const { notifications, uiSettings } = services; + const { toasts } = notifications; + const { samplerShardSize, visibleFieldTypes, showEmptyFields } = dataVisualizerListState; + + const [lastRefresh, setLastRefresh] = useState(0); + + const { + currentSavedSearch, + currentIndexPattern, + currentQuery, + currentFilters, + visibleFieldNames, + } = useMemo( + () => ({ + currentSavedSearch: input?.savedSearch, + currentIndexPattern: input.indexPattern, + currentQuery: input?.query, + visibleFieldNames: input?.visibleFieldNames ?? [], + currentFilters: input?.filters, + }), + [input] + ); + + const { searchQueryLanguage, searchString, searchQuery } = useMemo(() => { + const searchData = getEsQueryFromSavedSearch({ + indexPattern: currentIndexPattern, + uiSettings, + savedSearch: currentSavedSearch, + query: currentQuery, + filters: currentFilters, + }); + + if (searchData === undefined || dataVisualizerListState.searchString !== '') { + return { + searchQuery: dataVisualizerListState.searchQuery, + searchString: dataVisualizerListState.searchString, + searchQueryLanguage: dataVisualizerListState.searchQueryLanguage, + }; + } else { + return { + searchQuery: searchData.searchQuery, + searchString: searchData.searchString, + searchQueryLanguage: searchData.queryLanguage, + }; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + currentSavedSearch, + currentIndexPattern, + dataVisualizerListState, + currentQuery, + currentFilters, + ]); + + const [overallStats, setOverallStats] = useState(defaults.overallStats); + + const [documentCountStats, setDocumentCountStats] = useState(defaults.documentCountStats); + const [metricConfigs, setMetricConfigs] = useState(defaults.metricConfigs); + const [metricsLoaded, setMetricsLoaded] = useState(defaults.metricsLoaded); + const [metricsStats, setMetricsStats] = useState(); + + const [nonMetricConfigs, setNonMetricConfigs] = useState(defaults.nonMetricConfigs); + const [nonMetricsLoaded, setNonMetricsLoaded] = useState(defaults.nonMetricsLoaded); + + const dataLoader = useMemo( + () => new DataLoader(currentIndexPattern, toasts), + [currentIndexPattern, toasts] + ); + + const timefilter = useTimefilter({ + timeRangeSelector: currentIndexPattern?.timeFieldName !== undefined, + autoRefreshSelector: true, + }); + + useEffect(() => { + const timeUpdateSubscription = merge( + timefilter.getTimeUpdate$(), + dataVisualizerRefresh$ + ).subscribe(() => { + setLastRefresh(Date.now()); + }); + return () => { + timeUpdateSubscription.unsubscribe(); + }; + }); + + const getTimeBuckets = useCallback(() => { + return new TimeBuckets({ + [UI_SETTINGS.HISTOGRAM_MAX_BARS]: uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), + [UI_SETTINGS.HISTOGRAM_BAR_TARGET]: uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), + dateFormat: uiSettings.get('dateFormat'), + 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), + }); + }, [uiSettings]); + + const indexPatternFields: DataViewField[] = useMemo( + () => currentIndexPattern.fields, + [currentIndexPattern] + ); + + async function loadOverallStats() { + const tf = timefilter as any; + let earliest; + let latest; + + const activeBounds = tf.getActiveBounds(); + + if (currentIndexPattern.timeFieldName !== undefined && activeBounds === undefined) { + return; + } + + if (currentIndexPattern.timeFieldName !== undefined) { + earliest = activeBounds.min.valueOf(); + latest = activeBounds.max.valueOf(); + } + + try { + const allStats = await dataLoader.loadOverallData( + searchQuery, + samplerShardSize, + earliest, + latest + ); + // Because load overall stats perform queries in batches + // there could be multiple errors + if (Array.isArray(allStats.errors) && allStats.errors.length > 0) { + allStats.errors.forEach((err: any) => { + dataLoader.displayError(extractErrorProperties(err)); + }); + } + setOverallStats(allStats); + } catch (err) { + dataLoader.displayError(err.body ?? err); + } + } + + const createMetricCards = useCallback(() => { + const configs: FieldVisConfig[] = []; + const aggregatableExistsFields: any[] = overallStats.aggregatableExistsFields || []; + + const allMetricFields = indexPatternFields.filter((f) => { + return ( + f.type === KBN_FIELD_TYPES.NUMBER && + f.displayName !== undefined && + dataLoader.isDisplayField(f.displayName) === true + ); + }); + const metricExistsFields = allMetricFields.filter((f) => { + return aggregatableExistsFields.find((existsF) => { + return existsF.fieldName === f.spec.name; + }); + }); + + // Add a config for 'document count', identified by no field name if indexpattern is time based. + if (currentIndexPattern.timeFieldName !== undefined) { + configs.push({ + type: JOB_FIELD_TYPES.NUMBER, + existsInDocs: true, + loading: true, + aggregatable: true, + }); + } + + if (metricsLoaded === false) { + setMetricsLoaded(true); + return; + } + + let aggregatableFields: any[] = overallStats.aggregatableExistsFields; + if (allMetricFields.length !== metricExistsFields.length && metricsLoaded === true) { + aggregatableFields = aggregatableFields.concat(overallStats.aggregatableNotExistsFields); + } + + const metricFieldsToShow = + metricsLoaded === true && showEmptyFields === true ? allMetricFields : metricExistsFields; + + metricFieldsToShow.forEach((field) => { + const fieldData = aggregatableFields.find((f) => { + return f.fieldName === field.spec.name; + }); + + const metricConfig: FieldVisConfig = { + ...(fieldData ? fieldData : {}), + fieldFormat: currentIndexPattern.getFormatterForField(field), + type: JOB_FIELD_TYPES.NUMBER, + loading: true, + aggregatable: true, + deletable: field.runtimeField !== undefined, + }; + if (field.displayName !== metricConfig.fieldName) { + metricConfig.displayName = field.displayName; + } + + configs.push(metricConfig); + }); + + setMetricsStats({ + totalMetricFieldsCount: allMetricFields.length, + visibleMetricsCount: metricFieldsToShow.length, + }); + setMetricConfigs(configs); + }, [ + currentIndexPattern, + dataLoader, + indexPatternFields, + metricsLoaded, + overallStats, + showEmptyFields, + ]); + + const createNonMetricCards = useCallback(() => { + const allNonMetricFields = indexPatternFields.filter((f) => { + return ( + f.type !== KBN_FIELD_TYPES.NUMBER && + f.displayName !== undefined && + dataLoader.isDisplayField(f.displayName) === true + ); + }); + // Obtain the list of all non-metric fields which appear in documents + // (aggregatable or not aggregatable). + const populatedNonMetricFields: any[] = []; // Kibana index pattern non metric fields. + let nonMetricFieldData: any[] = []; // Basic non metric field data loaded from requesting overall stats. + const aggregatableExistsFields: any[] = overallStats.aggregatableExistsFields || []; + const nonAggregatableExistsFields: any[] = overallStats.nonAggregatableExistsFields || []; + + allNonMetricFields.forEach((f) => { + const checkAggregatableField = aggregatableExistsFields.find( + (existsField) => existsField.fieldName === f.spec.name + ); + + if (checkAggregatableField !== undefined) { + populatedNonMetricFields.push(f); + nonMetricFieldData.push(checkAggregatableField); + } else { + const checkNonAggregatableField = nonAggregatableExistsFields.find( + (existsField) => existsField.fieldName === f.spec.name + ); + + if (checkNonAggregatableField !== undefined) { + populatedNonMetricFields.push(f); + nonMetricFieldData.push(checkNonAggregatableField); + } + } + }); + + if (nonMetricsLoaded === false) { + setNonMetricsLoaded(true); + return; + } + + if (allNonMetricFields.length !== nonMetricFieldData.length && showEmptyFields === true) { + // Combine the field data obtained from Elasticsearch into a single array. + nonMetricFieldData = nonMetricFieldData.concat( + overallStats.aggregatableNotExistsFields, + overallStats.nonAggregatableNotExistsFields + ); + } + + const nonMetricFieldsToShow = showEmptyFields ? allNonMetricFields : populatedNonMetricFields; + + const configs: FieldVisConfig[] = []; + + nonMetricFieldsToShow.forEach((field) => { + const fieldData = nonMetricFieldData.find((f) => f.fieldName === field.spec.name); + + const nonMetricConfig = { + ...(fieldData ? fieldData : {}), + fieldFormat: currentIndexPattern.getFormatterForField(field), + aggregatable: field.aggregatable, + scripted: field.scripted, + loading: fieldData?.existsInDocs, + deletable: field.runtimeField !== undefined, + }; + + // Map the field type from the Kibana index pattern to the field type + // used in the data visualizer. + const dataVisualizerType = kbnTypeToJobType(field); + if (dataVisualizerType !== undefined) { + nonMetricConfig.type = dataVisualizerType; + } else { + // Add a flag to indicate that this is one of the 'other' Kibana + // field types that do not yet have a specific card type. + nonMetricConfig.type = field.type; + nonMetricConfig.isUnsupportedType = true; + } + + if (field.displayName !== nonMetricConfig.fieldName) { + nonMetricConfig.displayName = field.displayName; + } + + configs.push(nonMetricConfig); + }); + + setNonMetricConfigs(configs); + }, [ + currentIndexPattern, + dataLoader, + indexPatternFields, + nonMetricsLoaded, + overallStats, + showEmptyFields, + ]); + + async function loadMetricFieldStats() { + // Only request data for fields that exist in documents. + if (metricConfigs.length === 0) { + return; + } + + const configsToLoad = metricConfigs.filter( + (config) => config.existsInDocs === true && config.loading === true + ); + if (configsToLoad.length === 0) { + return; + } + + // Pass the field name, type and cardinality in the request. + // Top values will be obtained on a sample if cardinality > 100000. + const existMetricFields: FieldRequestConfig[] = configsToLoad.map((config) => { + const props = { fieldName: config.fieldName, type: config.type, cardinality: 0 }; + if (config.stats !== undefined && config.stats.cardinality !== undefined) { + props.cardinality = config.stats.cardinality; + } + return props; + }); + + // Obtain the interval to use for date histogram aggregations + // (such as the document count chart). Aim for 75 bars. + const buckets = getTimeBuckets(); + + const tf = timefilter as any; + let earliest: number | undefined; + let latest: number | undefined; + if (currentIndexPattern.timeFieldName !== undefined) { + earliest = tf.getActiveBounds().min.valueOf(); + latest = tf.getActiveBounds().max.valueOf(); + } + + const bounds = tf.getActiveBounds(); + const BAR_TARGET = 75; + buckets.setInterval('auto'); + buckets.setBounds(bounds); + buckets.setBarTarget(BAR_TARGET); + const aggInterval = buckets.getInterval(); + + try { + const metricFieldStats = await dataLoader.loadFieldStats( + searchQuery, + samplerShardSize, + earliest, + latest, + existMetricFields, + aggInterval.asMilliseconds() + ); + + // Add the metric stats to the existing stats in the corresponding config. + const configs: FieldVisConfig[] = []; + metricConfigs.forEach((config) => { + const configWithStats = { ...config }; + if (config.fieldName !== undefined) { + configWithStats.stats = { + ...configWithStats.stats, + ...metricFieldStats.find( + (fieldStats: any) => fieldStats.fieldName === config.fieldName + ), + }; + configWithStats.loading = false; + configs.push(configWithStats); + } else { + // Document count card. + configWithStats.stats = metricFieldStats.find( + (fieldStats: any) => fieldStats.fieldName === undefined + ); + + if (configWithStats.stats !== undefined) { + // Add earliest / latest of timefilter for setting x axis domain. + configWithStats.stats.timeRangeEarliest = earliest; + configWithStats.stats.timeRangeLatest = latest; + } + setDocumentCountStats(configWithStats); + } + }); + + setMetricConfigs(configs); + } catch (err) { + dataLoader.displayError(err); + } + } + + async function loadNonMetricFieldStats() { + // Only request data for fields that exist in documents. + if (nonMetricConfigs.length === 0) { + return; + } + + const configsToLoad = nonMetricConfigs.filter( + (config) => config.existsInDocs === true && config.loading === true + ); + if (configsToLoad.length === 0) { + return; + } + + // Pass the field name, type and cardinality in the request. + // Top values will be obtained on a sample if cardinality > 100000. + const existNonMetricFields: FieldRequestConfig[] = configsToLoad.map((config) => { + const props = { fieldName: config.fieldName, type: config.type, cardinality: 0 }; + if (config.stats !== undefined && config.stats.cardinality !== undefined) { + props.cardinality = config.stats.cardinality; + } + return props; + }); + + const tf = timefilter as any; + let earliest; + let latest; + if (currentIndexPattern.timeFieldName !== undefined) { + earliest = tf.getActiveBounds().min.valueOf(); + latest = tf.getActiveBounds().max.valueOf(); + } + + try { + const nonMetricFieldStats = await dataLoader.loadFieldStats( + searchQuery, + samplerShardSize, + earliest, + latest, + existNonMetricFields + ); + + // Add the field stats to the existing stats in the corresponding config. + const configs: FieldVisConfig[] = []; + nonMetricConfigs.forEach((config) => { + const configWithStats = { ...config }; + if (config.fieldName !== undefined) { + configWithStats.stats = { + ...configWithStats.stats, + ...nonMetricFieldStats.find( + (fieldStats: any) => fieldStats.fieldName === config.fieldName + ), + }; + } + configWithStats.loading = false; + configs.push(configWithStats); + }); + + setNonMetricConfigs(configs); + } catch (err) { + dataLoader.displayError(err); + } + } + + useEffect(() => { + loadOverallStats(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery, samplerShardSize, lastRefresh]); + + useEffect(() => { + createMetricCards(); + createNonMetricCards(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [overallStats, showEmptyFields]); + + useEffect(() => { + loadMetricFieldStats(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [metricConfigs]); + + useEffect(() => { + loadNonMetricFieldStats(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [nonMetricConfigs]); + + useEffect(() => { + createMetricCards(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [metricsLoaded]); + + useEffect(() => { + createNonMetricCards(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [nonMetricsLoaded]); + + const configs = useMemo(() => { + let combinedConfigs = [...nonMetricConfigs, ...metricConfigs]; + if (visibleFieldTypes && visibleFieldTypes.length > 0) { + combinedConfigs = combinedConfigs.filter( + (config) => visibleFieldTypes.findIndex((field) => field === config.type) > -1 + ); + } + if (visibleFieldNames && visibleFieldNames.length > 0) { + combinedConfigs = combinedConfigs.filter( + (config) => visibleFieldNames.findIndex((field) => field === config.fieldName) > -1 + ); + } + + return combinedConfigs; + }, [nonMetricConfigs, metricConfigs, visibleFieldTypes, visibleFieldNames]); + + // Some actions open up fly-out or popup + // This variable is used to keep track of them and clean up when unmounting + const actionFlyoutRef = useRef<() => void | undefined>(); + useEffect(() => { + const ref = actionFlyoutRef; + return () => { + // Clean up any of the flyout/editor opened from the actions + if (ref.current) { + ref.current(); + } + }; + }, []); + + // Inject custom action column for the index based visualizer + // Hide the column completely if no access to any of the plugins + const extendedColumns = useMemo(() => { + const actions = getActions( + input.indexPattern, + { lens: services.lens }, + { + searchQueryLanguage, + searchString, + }, + actionFlyoutRef + ); + if (!Array.isArray(actions) || actions.length < 1) return; + + const actionColumn: EuiTableActionsColumnType = { + name: i18n.translate('xpack.dataVisualizer.index.dataGrid.actionsColumnLabel', { + defaultMessage: 'Actions', + }), + actions, + width: '70px', + }; + + return [actionColumn]; + }, [input.indexPattern, services, searchQueryLanguage, searchString]); + + return { + configs, + searchQueryLanguage, + searchString, + searchQuery, + extendedColumns, + documentCountStats, + metricsStats, + loaded: metricsLoaded && nonMetricsLoaded, + }; +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/index.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/index.ts new file mode 100644 index 0000000000000..add99a8d2501d --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CoreSetup } from 'kibana/public'; +import { EmbeddableSetup } from '../../../../../../../src/plugins/embeddable/public'; +import { DataVisualizerGridEmbeddableFactory } from './grid_embeddable/grid_embeddable_factory'; +import { DataVisualizerPluginStart, DataVisualizerStartDependencies } from '../../../plugin'; + +export function registerEmbeddables( + embeddable: EmbeddableSetup, + core: CoreSetup +) { + const dataVisualizerGridEmbeddableFactory = new DataVisualizerGridEmbeddableFactory( + core.getStartServices + ); + embeddable.registerEmbeddableFactory( + dataVisualizerGridEmbeddableFactory.type, + dataVisualizerGridEmbeddableFactory + ); +} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx index a474ed3521580..83e013703c1fc 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx @@ -9,7 +9,6 @@ import React, { FC, useCallback, useEffect, useState } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { parse, stringify } from 'query-string'; import { isEqual } from 'lodash'; -// @ts-ignore import { encode } from 'rison-node'; import { SimpleSavedObject } from 'kibana/public'; import { i18n } from '@kbn/i18n'; @@ -29,7 +28,7 @@ import { isRisonSerializationRequired, } from '../common/util/url_state'; import { useDataVisualizerKibana } from '../kibana_context'; -import { IndexPattern } from '../../../../../../src/plugins/data/common'; +import { DataView } from '../../../../../../src/plugins/data/common'; import { ResultLink } from '../common/components/results_links'; export type IndexDataVisualizerSpec = typeof IndexDataVisualizer; @@ -51,9 +50,7 @@ export const DataVisualizerUrlStateContextProvider: FC( - undefined - ); + const [currentIndexPattern, setCurrentIndexPattern] = useState(undefined); const [currentSavedSearch, setCurrentSavedSearch] = useState | null>( null ); diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/locator/locator.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/locator/locator.ts index c26a668bd04ab..aab67d0b52aec 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/locator/locator.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/locator/locator.ts @@ -4,8 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -// @ts-ignore import { encode } from 'rison-node'; import { stringify } from 'query-string'; import { SerializableRecord } from '@kbn/utility-types'; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts index 43d815f6e9d41..ad3229676b31b 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts @@ -6,7 +6,7 @@ */ import { - getQueryFromSavedSearch, + getQueryFromSavedSearchObject, createMergedEsQuery, getEsQueryFromSavedSearch, } from './saved_search_utils'; @@ -82,9 +82,9 @@ const kqlSavedSearch: SavedSearch = { }, }; -describe('getQueryFromSavedSearch()', () => { +describe('getQueryFromSavedSearchObject()', () => { it('should return parsed searchSourceJSON with query and filter', () => { - expect(getQueryFromSavedSearch(luceneSavedSearchObj)).toEqual({ + expect(getQueryFromSavedSearchObject(luceneSavedSearchObj)).toEqual({ filter: [ { $state: { store: 'appState' }, @@ -106,7 +106,7 @@ describe('getQueryFromSavedSearch()', () => { query: { language: 'lucene', query: 'responsetime:>50' }, version: true, }); - expect(getQueryFromSavedSearch(kqlSavedSearch)).toEqual({ + expect(getQueryFromSavedSearchObject(kqlSavedSearch)).toEqual({ filter: [ { $state: { store: 'appState' }, @@ -130,7 +130,7 @@ describe('getQueryFromSavedSearch()', () => { }); }); it('should return undefined if invalid searchSourceJSON', () => { - expect(getQueryFromSavedSearch(luceneInvalidSavedSearchObj)).toEqual(undefined); + expect(getQueryFromSavedSearchObject(luceneInvalidSavedSearchObj)).toEqual(undefined); }); }); diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts index 80a2069aab1a8..1401b1038b8f2 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts @@ -16,17 +16,31 @@ import { Filter, } from '@kbn/es-query'; import { isSavedSearchSavedObject, SavedSearchSavedObject } from '../../../../common/types'; -import { IndexPattern } from '../../../../../../../src/plugins/data/common'; +import { IndexPattern, SearchSource } from '../../../../../../../src/plugins/data/common'; import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '../types/combined_query'; import { SavedSearch } from '../../../../../../../src/plugins/discover/public'; import { getEsQueryConfig } from '../../../../../../../src/plugins/data/common'; import { FilterManager } from '../../../../../../../src/plugins/data/public'; +const DEFAULT_QUERY = { + bool: { + must: [ + { + match_all: {}, + }, + ], + }, +}; + +export function getDefaultQuery() { + return cloneDeep(DEFAULT_QUERY); +} + /** * Parse the stringified searchSourceJSON * from a saved search or saved search object */ -export function getQueryFromSavedSearch(savedSearch: SavedSearchSavedObject | SavedSearch) { +export function getQueryFromSavedSearchObject(savedSearch: SavedSearchSavedObject | SavedSearch) { const search = isSavedSearchSavedObject(savedSearch) ? savedSearch?.attributes?.kibanaSavedObjectMeta : // @ts-expect-error kibanaSavedObjectMeta does exist @@ -69,20 +83,22 @@ export function createMergedEsQuery( if (query.query !== '') { combinedQuery = toElasticsearchQuery(ast, indexPattern); } - const filterQuery = buildQueryFromFilters(filters, indexPattern); + if (combinedQuery.bool !== undefined) { + const filterQuery = buildQueryFromFilters(filters, indexPattern); - if (Array.isArray(combinedQuery.bool.filter) === false) { - combinedQuery.bool.filter = - combinedQuery.bool.filter === undefined ? [] : [combinedQuery.bool.filter]; - } + if (Array.isArray(combinedQuery.bool.filter) === false) { + combinedQuery.bool.filter = + combinedQuery.bool.filter === undefined ? [] : [combinedQuery.bool.filter]; + } - if (Array.isArray(combinedQuery.bool.must_not) === false) { - combinedQuery.bool.must_not = - combinedQuery.bool.must_not === undefined ? [] : [combinedQuery.bool.must_not]; - } + if (Array.isArray(combinedQuery.bool.must_not) === false) { + combinedQuery.bool.must_not = + combinedQuery.bool.must_not === undefined ? [] : [combinedQuery.bool.must_not]; + } - combinedQuery.bool.filter = [...combinedQuery.bool.filter, ...filterQuery.filter]; - combinedQuery.bool.must_not = [...combinedQuery.bool.must_not, ...filterQuery.must_not]; + combinedQuery.bool.filter = [...combinedQuery.bool.filter, ...filterQuery.filter]; + combinedQuery.bool.must_not = [...combinedQuery.bool.must_not, ...filterQuery.must_not]; + } } else { combinedQuery = buildEsQuery( indexPattern, @@ -115,10 +131,31 @@ export function getEsQueryFromSavedSearch({ }) { if (!indexPattern || !savedSearch) return; - const savedSearchData = getQueryFromSavedSearch(savedSearch); const userQuery = query; const userFilters = filters; + // If saved search has a search source with nested parent + // e.g. a search coming from Dashboard saved search embeddable + // which already combines both the saved search's original query/filters and the Dashboard's + // then no need to process any further + if ( + savedSearch && + 'searchSource' in savedSearch && + savedSearch?.searchSource instanceof SearchSource && + savedSearch.searchSource.getParent() !== undefined && + userQuery + ) { + return { + searchQuery: savedSearch.searchSource.getSearchRequestBody()?.query ?? getDefaultQuery(), + searchString: userQuery.query, + queryLanguage: userQuery.language as SearchQueryLanguage, + }; + } + + // If saved search is an json object with the original query and filter + // retrieve the parsed query and filter + const savedSearchData = getQueryFromSavedSearchObject(savedSearch); + // If no saved search available, use user's query and filters if (!savedSearchData && userQuery) { if (filterManager && userFilters) filterManager.setFilters(userFilters); @@ -137,7 +174,8 @@ export function getEsQueryFromSavedSearch({ }; } - // If saved search available, merge saved search with latest user query or filters differ from extracted saved search data + // If saved search available, merge saved search with latest user query or filters + // which might differ from extracted saved search data if (savedSearchData) { const currentQuery = userQuery ?? savedSearchData?.query; const currentFilters = userFilters ?? savedSearchData?.filter; @@ -158,17 +196,3 @@ export function getEsQueryFromSavedSearch({ }; } } - -const DEFAULT_QUERY = { - bool: { - must: [ - { - match_all: {}, - }, - ], - }, -}; - -export function getDefaultQuery() { - return cloneDeep(DEFAULT_QUERY); -} diff --git a/x-pack/plugins/data_visualizer/public/plugin.ts b/x-pack/plugins/data_visualizer/public/plugin.ts index 112294f4b246f..df1a5ea406d76 100644 --- a/x-pack/plugins/data_visualizer/public/plugin.ts +++ b/x-pack/plugins/data_visualizer/public/plugin.ts @@ -6,7 +6,7 @@ */ import { CoreSetup, CoreStart } from 'kibana/public'; -import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; +import type { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import type { SharePluginStart } from '../../../../src/plugins/share/public'; import { Plugin } from '../../../../src/core/public'; @@ -21,9 +21,11 @@ import type { IndexPatternFieldEditorStart } from '../../../../src/plugins/index import { getFileDataVisualizerComponent, getIndexDataVisualizerComponent } from './api'; import { getMaxBytesFormatted } from './application/common/util/get_max_bytes'; import { registerHomeAddData, registerHomeFeatureCatalogue } from './register_home'; +import { registerEmbeddables } from './application/index_data_visualizer/embeddables'; export interface DataVisualizerSetupDependencies { home?: HomePublicPluginSetup; + embeddable: EmbeddableSetup; } export interface DataVisualizerStartDependencies { data: DataPublicPluginStart; @@ -56,6 +58,9 @@ export class DataVisualizerPlugin registerHomeAddData(plugins.home); registerHomeFeatureCatalogue(plugins.home); } + if (plugins.embeddable) { + registerEmbeddables(plugins.embeddable, core); + } } public start(core: CoreStart, plugins: DataVisualizerStartDependencies) { diff --git a/x-pack/plugins/data_visualizer/tsconfig.json b/x-pack/plugins/data_visualizer/tsconfig.json index 3b424ef8b9f65..df41fdbd62663 100644 --- a/x-pack/plugins/data_visualizer/tsconfig.json +++ b/x-pack/plugins/data_visualizer/tsconfig.json @@ -6,9 +6,18 @@ "declaration": true, "declarationMap": true }, - "include": ["common/**/*", "public/**/*", "server/**/*"], + "include": [ + "../../../typings/**/*", + "common/**/*", + "public/**/*", + "scripts/**/*", + "server/**/*", + "types/**/*" + ], "references": [ { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../../src/plugins/data/tsconfig.json" }, { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, { "path": "../../../src/plugins/custom_integrations/tsconfig.json" }, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/use_saved_search.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/use_saved_search.ts index 324db0d6b2ad4..41973b5ec2d01 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/use_saved_search.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/use_saved_search.ts @@ -15,7 +15,7 @@ import { import { estypes } from '@elastic/elasticsearch'; import { useMlContext } from '../../../../../contexts/ml'; import { SEARCH_QUERY_LANGUAGE } from '../../../../../../../common/constants/search'; -import { getQueryFromSavedSearch } from '../../../../../util/index_utils'; +import { getQueryFromSavedSearchObject } from '../../../../../util/index_utils'; // `undefined` is used for a non-initialized state // `null` is set if no saved search is used @@ -40,7 +40,7 @@ export function useSavedSearch() { let qryString; if (currentSavedSearch !== null) { - const { query } = getQueryFromSavedSearch(currentSavedSearch); + const { query } = getQueryFromSavedSearchObject(currentSavedSearch); const queryLanguage = query.language; qryString = query.query; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts b/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts index 5eae60900e09f..ebab3769fbe57 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts @@ -18,7 +18,7 @@ import { IUiSettingsClient } from 'kibana/public'; import { getEsQueryConfig } from '../../../../../../../../src/plugins/data/public'; import { SEARCH_QUERY_LANGUAGE } from '../../../../../common/constants/search'; import { SavedSearchSavedObject } from '../../../../../common/types/kibana'; -import { getQueryFromSavedSearch } from '../../../util/index_utils'; +import { getQueryFromSavedSearchObject } from '../../../util/index_utils'; // Provider for creating the items used for searching and job creation. @@ -52,7 +52,7 @@ export function createSearchItems( let combinedQuery: any = getDefaultDatafeedQuery(); if (savedSearch !== null) { - const data = getQueryFromSavedSearch(savedSearch); + const data = getQueryFromSavedSearchObject(savedSearch); query = data.query; const filter = data.filter; diff --git a/x-pack/plugins/ml/public/application/util/index_utils.ts b/x-pack/plugins/ml/public/application/util/index_utils.ts index e4c18308bf017..b105761e5ebcf 100644 --- a/x-pack/plugins/ml/public/application/util/index_utils.ts +++ b/x-pack/plugins/ml/public/application/util/index_utils.ts @@ -80,7 +80,7 @@ export async function getIndexPatternAndSavedSearch(savedSearchId: string) { return resp; } -export function getQueryFromSavedSearch(savedSearch: SavedSearchSavedObject) { +export function getQueryFromSavedSearchObject(savedSearch: SavedSearchSavedObject) { const search = savedSearch.attributes.kibanaSavedObjectMeta as { searchSourceJSON: string }; return JSON.parse(search.searchSourceJSON) as { query: Query; diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index.ts b/x-pack/test/functional/apps/ml/data_visualizer/index.ts index 3e6b644a0b494..c1e5d0b4b6aae 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index.ts @@ -9,9 +9,10 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('data visualizer', function () { - this.tags(['skipFirefox']); + this.tags(['skipFirefox', 'mlqa']); loadTestFile(require.resolve('./index_data_visualizer')); + loadTestFile(require.resolve('./index_data_visualizer_grid_in_discover')); loadTestFile(require.resolve('./index_data_visualizer_actions_panel')); loadTestFile(require.resolve('./index_data_visualizer_index_pattern_management')); loadTestFile(require.resolve('./file_data_visualizer')); diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts index 542f7f3116c94..ff0d489293682 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts @@ -6,374 +6,18 @@ */ import { FtrProviderContext } from '../../../ftr_provider_context'; -import { ML_JOB_FIELD_TYPES } from '../../../../../plugins/ml/common/constants/field_types'; -import { FieldVisConfig } from '../../../../../plugins/data_visualizer/public/application/common/components/stats_table/types'; - -interface MetricFieldVisConfig extends FieldVisConfig { - statsMaxDecimalPlaces: number; - docCountFormatted: string; - topValuesCount: number; - viewableInLens: boolean; -} - -interface NonMetricFieldVisConfig extends FieldVisConfig { - docCountFormatted: string; - exampleCount: number; - viewableInLens: boolean; -} - -interface TestData { - suiteTitle: string; - sourceIndexOrSavedSearch: string; - fieldNameFilters: string[]; - fieldTypeFilters: string[]; - rowsPerPage?: 10 | 25 | 50; - sampleSizeValidations: Array<{ - size: number; - expected: { field: string; docCountFormatted: string }; - }>; - expected: { - totalDocCountFormatted: string; - metricFields?: MetricFieldVisConfig[]; - nonMetricFields?: NonMetricFieldVisConfig[]; - emptyFields: string[]; - visibleMetricFieldsCount: number; - totalMetricFieldsCount: number; - populatedFieldsCount: number; - totalFieldsCount: number; - fieldNameFiltersResultCount: number; - fieldTypeFiltersResultCount: number; - }; -} +import { TestData, MetricFieldVisConfig } from './types'; +import { + farequoteDataViewTestData, + farequoteKQLSearchTestData, + farequoteLuceneSearchTestData, + sampleLogTestData, +} from './index_test_data'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - const farequoteDataViewTestData: TestData = { - suiteTitle: 'data view', - sourceIndexOrSavedSearch: 'ft_farequote', - fieldNameFilters: ['airline', '@timestamp'], - fieldTypeFilters: [ML_JOB_FIELD_TYPES.KEYWORD], - sampleSizeValidations: [ - { size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } }, - { size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } }, - ], - expected: { - totalDocCountFormatted: '86,274', - metricFields: [ - { - fieldName: 'responsetime', - type: ML_JOB_FIELD_TYPES.NUMBER, - existsInDocs: true, - aggregatable: true, - loading: false, - docCountFormatted: '5000 (100%)', - statsMaxDecimalPlaces: 3, - topValuesCount: 10, - viewableInLens: true, - }, - ], - nonMetricFields: [ - { - fieldName: '@timestamp', - type: ML_JOB_FIELD_TYPES.DATE, - existsInDocs: true, - aggregatable: true, - loading: false, - docCountFormatted: '5000 (100%)', - exampleCount: 2, - viewableInLens: true, - }, - { - fieldName: '@version', - type: ML_JOB_FIELD_TYPES.TEXT, - existsInDocs: true, - aggregatable: false, - loading: false, - exampleCount: 1, - docCountFormatted: '', - viewableInLens: false, - }, - { - fieldName: '@version.keyword', - type: ML_JOB_FIELD_TYPES.KEYWORD, - existsInDocs: true, - aggregatable: true, - loading: false, - exampleCount: 1, - docCountFormatted: '5000 (100%)', - viewableInLens: true, - }, - { - fieldName: 'airline', - type: ML_JOB_FIELD_TYPES.KEYWORD, - existsInDocs: true, - aggregatable: true, - loading: false, - exampleCount: 10, - docCountFormatted: '5000 (100%)', - viewableInLens: true, - }, - { - fieldName: 'type', - type: ML_JOB_FIELD_TYPES.TEXT, - existsInDocs: true, - aggregatable: false, - loading: false, - exampleCount: 1, - docCountFormatted: '', - viewableInLens: false, - }, - { - fieldName: 'type.keyword', - type: ML_JOB_FIELD_TYPES.KEYWORD, - existsInDocs: true, - aggregatable: true, - loading: false, - exampleCount: 1, - docCountFormatted: '5000 (100%)', - viewableInLens: true, - }, - ], - emptyFields: ['sourcetype'], - visibleMetricFieldsCount: 1, - totalMetricFieldsCount: 1, - populatedFieldsCount: 7, - totalFieldsCount: 8, - fieldNameFiltersResultCount: 2, - fieldTypeFiltersResultCount: 3, - }, - }; - - const farequoteKQLSearchTestData: TestData = { - suiteTitle: 'KQL saved search', - sourceIndexOrSavedSearch: 'ft_farequote_kuery', - fieldNameFilters: ['@version'], - fieldTypeFilters: [ML_JOB_FIELD_TYPES.DATE, ML_JOB_FIELD_TYPES.TEXT], - sampleSizeValidations: [ - { size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } }, - { size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } }, - ], - expected: { - totalDocCountFormatted: '34,415', - metricFields: [ - { - fieldName: 'responsetime', - type: ML_JOB_FIELD_TYPES.NUMBER, - existsInDocs: true, - aggregatable: true, - loading: false, - docCountFormatted: '5000 (100%)', - statsMaxDecimalPlaces: 3, - topValuesCount: 10, - viewableInLens: true, - }, - ], - nonMetricFields: [ - { - fieldName: '@timestamp', - type: ML_JOB_FIELD_TYPES.DATE, - existsInDocs: true, - aggregatable: true, - loading: false, - docCountFormatted: '5000 (100%)', - exampleCount: 2, - viewableInLens: true, - }, - { - fieldName: '@version', - type: ML_JOB_FIELD_TYPES.TEXT, - existsInDocs: true, - aggregatable: false, - loading: false, - exampleCount: 1, - docCountFormatted: '', - viewableInLens: false, - }, - { - fieldName: '@version.keyword', - type: ML_JOB_FIELD_TYPES.KEYWORD, - existsInDocs: true, - aggregatable: true, - loading: false, - exampleCount: 1, - docCountFormatted: '5000 (100%)', - viewableInLens: true, - }, - { - fieldName: 'airline', - type: ML_JOB_FIELD_TYPES.KEYWORD, - existsInDocs: true, - aggregatable: true, - loading: false, - exampleCount: 5, - docCountFormatted: '5000 (100%)', - viewableInLens: true, - }, - { - fieldName: 'type', - type: ML_JOB_FIELD_TYPES.TEXT, - existsInDocs: true, - aggregatable: false, - loading: false, - exampleCount: 1, - docCountFormatted: '', - viewableInLens: false, - }, - { - fieldName: 'type.keyword', - type: ML_JOB_FIELD_TYPES.KEYWORD, - existsInDocs: true, - aggregatable: true, - loading: false, - exampleCount: 1, - docCountFormatted: '5000 (100%)', - viewableInLens: true, - }, - ], - emptyFields: ['sourcetype'], - visibleMetricFieldsCount: 1, - totalMetricFieldsCount: 1, - populatedFieldsCount: 7, - totalFieldsCount: 8, - fieldNameFiltersResultCount: 1, - fieldTypeFiltersResultCount: 3, - }, - }; - - const farequoteLuceneSearchTestData: TestData = { - suiteTitle: 'lucene saved search', - sourceIndexOrSavedSearch: 'ft_farequote_lucene', - fieldNameFilters: ['@version.keyword', 'type'], - fieldTypeFilters: [ML_JOB_FIELD_TYPES.NUMBER], - sampleSizeValidations: [ - { size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } }, - { size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } }, - ], - expected: { - totalDocCountFormatted: '34,416', - metricFields: [ - { - fieldName: 'responsetime', - type: ML_JOB_FIELD_TYPES.NUMBER, - existsInDocs: true, - aggregatable: true, - loading: false, - docCountFormatted: '5000 (100%)', - statsMaxDecimalPlaces: 3, - topValuesCount: 10, - viewableInLens: true, - }, - ], - nonMetricFields: [ - { - fieldName: '@timestamp', - type: ML_JOB_FIELD_TYPES.DATE, - existsInDocs: true, - aggregatable: true, - loading: false, - docCountFormatted: '5000 (100%)', - exampleCount: 2, - viewableInLens: true, - }, - { - fieldName: '@version', - type: ML_JOB_FIELD_TYPES.TEXT, - existsInDocs: true, - aggregatable: false, - loading: false, - exampleCount: 1, - docCountFormatted: '', - viewableInLens: false, - }, - { - fieldName: '@version.keyword', - type: ML_JOB_FIELD_TYPES.KEYWORD, - existsInDocs: true, - aggregatable: true, - loading: false, - exampleCount: 1, - docCountFormatted: '5000 (100%)', - viewableInLens: true, - }, - { - fieldName: 'airline', - type: ML_JOB_FIELD_TYPES.KEYWORD, - existsInDocs: true, - aggregatable: true, - loading: false, - exampleCount: 5, - docCountFormatted: '5000 (100%)', - viewableInLens: true, - }, - { - fieldName: 'type', - type: ML_JOB_FIELD_TYPES.TEXT, - existsInDocs: true, - aggregatable: false, - loading: false, - exampleCount: 1, - docCountFormatted: '', - viewableInLens: false, - }, - { - fieldName: 'type.keyword', - type: ML_JOB_FIELD_TYPES.KEYWORD, - existsInDocs: true, - aggregatable: true, - loading: false, - exampleCount: 1, - docCountFormatted: '5000 (100%)', - viewableInLens: true, - }, - ], - emptyFields: ['sourcetype'], - visibleMetricFieldsCount: 1, - totalMetricFieldsCount: 1, - populatedFieldsCount: 7, - totalFieldsCount: 8, - fieldNameFiltersResultCount: 2, - fieldTypeFiltersResultCount: 1, - }, - }; - - const sampleLogTestData: TestData = { - suiteTitle: 'geo point field', - sourceIndexOrSavedSearch: 'ft_module_sample_logs', - fieldNameFilters: ['geo.coordinates'], - fieldTypeFilters: [ML_JOB_FIELD_TYPES.GEO_POINT], - rowsPerPage: 50, - expected: { - totalDocCountFormatted: '408', - metricFields: [], - // only testing the geo_point fields - nonMetricFields: [ - { - fieldName: 'geo.coordinates', - type: ML_JOB_FIELD_TYPES.GEO_POINT, - existsInDocs: true, - aggregatable: true, - loading: false, - docCountFormatted: '408 (100%)', - exampleCount: 10, - viewableInLens: false, - }, - ], - emptyFields: [], - visibleMetricFieldsCount: 4, - totalMetricFieldsCount: 5, - populatedFieldsCount: 35, - totalFieldsCount: 36, - fieldNameFiltersResultCount: 1, - fieldTypeFiltersResultCount: 1, - }, - sampleSizeValidations: [ - { size: 1000, expected: { field: 'geo.coordinates', docCountFormatted: '408 (100%)' } }, - { size: 5000, expected: { field: '@timestamp', docCountFormatted: '408 (100%)' } }, - ], - }; - function runTests(testData: TestData) { it(`${testData.suiteTitle} loads the source data in the data visualizer`, async () => { await ml.testExecution.logTestStep( @@ -541,7 +185,7 @@ export default function ({ getService }: FtrProviderContext) { }); describe('with module_sample_logs ', function () { - // Run tests on full farequote index. + // Run tests on full ft_module_sample_logs index. it(`${sampleLogTestData.suiteTitle} loads the data visualizer selector page`, async () => { // Start navigation from the base of the ML app. await ml.navigation.navigateToMl(); diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_discover.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_discover.ts new file mode 100644 index 0000000000000..ba24684e13036 --- /dev/null +++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_discover.ts @@ -0,0 +1,172 @@ +/* + * 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 { TestData, MetricFieldVisConfig } from './types'; + +const SHOW_FIELD_STATISTICS = 'discover:showFieldStatistics'; +import { + farequoteDataViewTestData, + farequoteKQLSearchTestData, + farequoteLuceneFiltersSearchTestData, + farequoteKQLFiltersSearchTestData, + farequoteLuceneSearchTestData, + sampleLogTestData, +} from './index_test_data'; +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'settings']); + const ml = getService('ml'); + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const toasts = getService('toasts'); + + const selectIndexPattern = async (indexPattern: string) => { + await retry.tryForTime(2 * 1000, async () => { + await PageObjects.discover.selectIndexPattern(indexPattern); + const indexPatternTitle = await testSubjects.getVisibleText('indexPattern-switch-link'); + expect(indexPatternTitle).to.be(indexPattern); + }); + }; + + const clearAdvancedSetting = async (propertyName: string) => { + await retry.tryForTime(2 * 1000, async () => { + await PageObjects.common.navigateToUrl('management', 'kibana/settings', { + shouldUseHashForSubUrl: false, + }); + if ((await PageObjects.settings.getAdvancedSettingCheckbox(propertyName)) === 'true') { + await PageObjects.settings.clearAdvancedSettings(propertyName); + } + }); + }; + + const setAdvancedSettingCheckbox = async (propertyName: string, checkedState: boolean) => { + await retry.tryForTime(2 * 1000, async () => { + await PageObjects.common.navigateToUrl('management', 'kibana/settings', { + shouldUseHashForSubUrl: false, + }); + await testSubjects.click('settings'); + await toasts.dismissAllToasts(); + await PageObjects.settings.toggleAdvancedSettingCheckbox(propertyName, checkedState); + }); + }; + + function runTestsWhenDisabled(testData: TestData) { + it('should not show view mode toggle or Field stats table', async function () { + await PageObjects.common.navigateToApp('discover'); + if (testData.isSavedSearch) { + await retry.tryForTime(2 * 1000, async () => { + await PageObjects.discover.loadSavedSearch(testData.sourceIndexOrSavedSearch); + }); + } else { + await selectIndexPattern(testData.sourceIndexOrSavedSearch); + } + + await PageObjects.timePicker.setAbsoluteRange( + 'Jan 1, 2016 @ 00:00:00.000', + 'Nov 1, 2020 @ 00:00:00.000' + ); + + await PageObjects.discover.assertViewModeToggleNotExists(); + await PageObjects.discover.assertFieldStatsTableNotExists(); + }); + } + + function runTests(testData: TestData) { + describe(`with ${testData.suiteTitle}`, function () { + it(`displays the 'Field statistics' table content correctly`, async function () { + await PageObjects.common.navigateToApp('discover'); + if (testData.isSavedSearch) { + await retry.tryForTime(2 * 1000, async () => { + await PageObjects.discover.loadSavedSearch(testData.sourceIndexOrSavedSearch); + }); + } else { + await selectIndexPattern(testData.sourceIndexOrSavedSearch); + } + await PageObjects.timePicker.setAbsoluteRange( + 'Jan 1, 2016 @ 00:00:00.000', + 'Nov 1, 2020 @ 00:00:00.000' + ); + + await PageObjects.discover.assertHitCount(testData.expected.totalDocCountFormatted); + await PageObjects.discover.assertViewModeToggleExists(); + await PageObjects.discover.clickViewModeFieldStatsButton(); + await ml.testExecution.logTestStep( + 'displays details for metric fields and non-metric fields correctly' + ); + for (const fieldRow of testData.expected.metricFields as Array< + Required + >) { + await ml.dataVisualizerTable.assertNumberFieldContents( + fieldRow.fieldName, + fieldRow.docCountFormatted, + fieldRow.topValuesCount, + fieldRow.viewableInLens + ); + } + + for (const fieldRow of testData.expected.nonMetricFields!) { + await ml.dataVisualizerTable.assertNonMetricFieldContents( + fieldRow.type, + fieldRow.fieldName!, + fieldRow.docCountFormatted, + fieldRow.exampleCount, + fieldRow.viewableInLens, + false, + fieldRow.exampleContent + ); + } + }); + }); + } + + describe('field statistics in Discover', function () { + before(async function () { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/module_sample_logs'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + await ml.testResources.createIndexPatternIfNeeded('ft_module_sample_logs', '@timestamp'); + await ml.testResources.createSavedSearchFarequoteKueryIfNeeded(); + await ml.testResources.createSavedSearchFarequoteLuceneIfNeeded(); + await ml.testResources.createSavedSearchFarequoteFilterAndLuceneIfNeeded(); + await ml.testResources.createSavedSearchFarequoteFilterAndKueryIfNeeded(); + + await ml.securityUI.loginAsMlPowerUser(); + }); + + after(async function () { + await clearAdvancedSetting(SHOW_FIELD_STATISTICS); + }); + + describe('when enabled', function () { + before(async function () { + await setAdvancedSettingCheckbox(SHOW_FIELD_STATISTICS, true); + }); + + after(async function () { + await clearAdvancedSetting(SHOW_FIELD_STATISTICS); + }); + + runTests(farequoteDataViewTestData); + runTests(farequoteKQLSearchTestData); + runTests(farequoteLuceneSearchTestData); + runTests(farequoteKQLFiltersSearchTestData); + runTests(farequoteLuceneFiltersSearchTestData); + runTests(sampleLogTestData); + }); + + describe('when disabled', function () { + before(async function () { + // Ensure that the setting is set to default state which is false + await setAdvancedSettingCheckbox(SHOW_FIELD_STATISTICS, false); + }); + + runTestsWhenDisabled(farequoteDataViewTestData); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_test_data.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_test_data.ts new file mode 100644 index 0000000000000..6dd782487fdf8 --- /dev/null +++ b/x-pack/test/functional/apps/ml/data_visualizer/index_test_data.ts @@ -0,0 +1,533 @@ +/* + * 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 { TestData } from './types'; +import { ML_JOB_FIELD_TYPES } from '../../../../../plugins/ml/common/constants/field_types'; + +export const farequoteDataViewTestData: TestData = { + suiteTitle: 'farequote index pattern', + isSavedSearch: false, + sourceIndexOrSavedSearch: 'ft_farequote', + fieldNameFilters: ['airline', '@timestamp'], + fieldTypeFilters: [ML_JOB_FIELD_TYPES.KEYWORD], + sampleSizeValidations: [ + { size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } }, + { size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } }, + ], + expected: { + totalDocCountFormatted: '86,274', + metricFields: [ + { + fieldName: 'responsetime', + type: ML_JOB_FIELD_TYPES.NUMBER, + existsInDocs: true, + aggregatable: true, + loading: false, + docCountFormatted: '5000 (100%)', + statsMaxDecimalPlaces: 3, + topValuesCount: 10, + viewableInLens: true, + }, + ], + nonMetricFields: [ + { + fieldName: '@timestamp', + type: ML_JOB_FIELD_TYPES.DATE, + existsInDocs: true, + aggregatable: true, + loading: false, + docCountFormatted: '5000 (100%)', + exampleCount: 2, + viewableInLens: true, + }, + { + fieldName: '@version', + type: ML_JOB_FIELD_TYPES.TEXT, + existsInDocs: true, + aggregatable: false, + loading: false, + exampleCount: 1, + docCountFormatted: '', + viewableInLens: false, + }, + { + fieldName: '@version.keyword', + type: ML_JOB_FIELD_TYPES.KEYWORD, + existsInDocs: true, + aggregatable: true, + loading: false, + exampleCount: 1, + docCountFormatted: '5000 (100%)', + viewableInLens: true, + }, + { + fieldName: 'airline', + type: ML_JOB_FIELD_TYPES.KEYWORD, + existsInDocs: true, + aggregatable: true, + loading: false, + exampleCount: 10, + docCountFormatted: '5000 (100%)', + viewableInLens: true, + }, + { + fieldName: 'type', + type: ML_JOB_FIELD_TYPES.TEXT, + existsInDocs: true, + aggregatable: false, + loading: false, + exampleCount: 1, + docCountFormatted: '', + viewableInLens: false, + }, + { + fieldName: 'type.keyword', + type: ML_JOB_FIELD_TYPES.KEYWORD, + existsInDocs: true, + aggregatable: true, + loading: false, + exampleCount: 1, + docCountFormatted: '5000 (100%)', + viewableInLens: true, + }, + ], + emptyFields: ['sourcetype'], + visibleMetricFieldsCount: 1, + totalMetricFieldsCount: 1, + populatedFieldsCount: 7, + totalFieldsCount: 8, + fieldNameFiltersResultCount: 2, + fieldTypeFiltersResultCount: 3, + }, +}; + +export const farequoteKQLSearchTestData: TestData = { + suiteTitle: 'KQL saved search', + isSavedSearch: true, + sourceIndexOrSavedSearch: 'ft_farequote_kuery', + fieldNameFilters: ['@version'], + fieldTypeFilters: [ML_JOB_FIELD_TYPES.DATE, ML_JOB_FIELD_TYPES.TEXT], + sampleSizeValidations: [ + { size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } }, + { size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } }, + ], + expected: { + totalDocCountFormatted: '34,415', + metricFields: [ + { + fieldName: 'responsetime', + type: ML_JOB_FIELD_TYPES.NUMBER, + existsInDocs: true, + aggregatable: true, + loading: false, + docCountFormatted: '5000 (100%)', + statsMaxDecimalPlaces: 3, + topValuesCount: 10, + viewableInLens: true, + }, + ], + nonMetricFields: [ + { + fieldName: '@timestamp', + type: ML_JOB_FIELD_TYPES.DATE, + existsInDocs: true, + aggregatable: true, + loading: false, + docCountFormatted: '5000 (100%)', + exampleCount: 2, + viewableInLens: true, + }, + { + fieldName: '@version', + type: ML_JOB_FIELD_TYPES.TEXT, + existsInDocs: true, + aggregatable: false, + loading: false, + exampleCount: 1, + docCountFormatted: '', + viewableInLens: false, + }, + { + fieldName: '@version.keyword', + type: ML_JOB_FIELD_TYPES.KEYWORD, + existsInDocs: true, + aggregatable: true, + loading: false, + exampleCount: 1, + docCountFormatted: '5000 (100%)', + viewableInLens: true, + }, + { + fieldName: 'airline', + type: ML_JOB_FIELD_TYPES.KEYWORD, + existsInDocs: true, + aggregatable: true, + loading: false, + exampleCount: 5, + docCountFormatted: '5000 (100%)', + viewableInLens: true, + }, + { + fieldName: 'type', + type: ML_JOB_FIELD_TYPES.TEXT, + existsInDocs: true, + aggregatable: false, + loading: false, + exampleCount: 1, + docCountFormatted: '', + viewableInLens: false, + }, + { + fieldName: 'type.keyword', + type: ML_JOB_FIELD_TYPES.KEYWORD, + existsInDocs: true, + aggregatable: true, + loading: false, + exampleCount: 1, + docCountFormatted: '5000 (100%)', + viewableInLens: true, + }, + ], + emptyFields: ['sourcetype'], + visibleMetricFieldsCount: 1, + totalMetricFieldsCount: 1, + populatedFieldsCount: 7, + totalFieldsCount: 8, + fieldNameFiltersResultCount: 1, + fieldTypeFiltersResultCount: 3, + }, +}; + +export const farequoteKQLFiltersSearchTestData: TestData = { + suiteTitle: 'KQL saved search and filters', + isSavedSearch: true, + sourceIndexOrSavedSearch: 'ft_farequote_filter_and_kuery', + fieldNameFilters: ['@version'], + fieldTypeFilters: [ML_JOB_FIELD_TYPES.DATE, ML_JOB_FIELD_TYPES.TEXT], + sampleSizeValidations: [ + { size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } }, + { size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } }, + ], + expected: { + totalDocCountFormatted: '5,674', + metricFields: [ + { + fieldName: 'responsetime', + type: ML_JOB_FIELD_TYPES.NUMBER, + existsInDocs: true, + aggregatable: true, + loading: false, + docCountFormatted: '5000 (100%)', + statsMaxDecimalPlaces: 3, + topValuesCount: 10, + viewableInLens: true, + }, + ], + nonMetricFields: [ + { + fieldName: '@timestamp', + type: ML_JOB_FIELD_TYPES.DATE, + existsInDocs: true, + aggregatable: true, + loading: false, + docCountFormatted: '5000 (100%)', + exampleCount: 2, + viewableInLens: true, + }, + { + fieldName: '@version', + type: ML_JOB_FIELD_TYPES.TEXT, + existsInDocs: true, + aggregatable: false, + loading: false, + exampleCount: 1, + docCountFormatted: '', + viewableInLens: false, + }, + { + fieldName: '@version.keyword', + type: ML_JOB_FIELD_TYPES.KEYWORD, + existsInDocs: true, + aggregatable: true, + loading: false, + exampleCount: 1, + docCountFormatted: '5000 (100%)', + viewableInLens: true, + }, + { + fieldName: 'airline', + type: ML_JOB_FIELD_TYPES.KEYWORD, + existsInDocs: true, + aggregatable: true, + loading: false, + exampleCount: 1, + exampleContent: ['ASA'], + docCountFormatted: '5000 (100%)', + viewableInLens: true, + }, + { + fieldName: 'type', + type: ML_JOB_FIELD_TYPES.TEXT, + existsInDocs: true, + aggregatable: false, + loading: false, + exampleCount: 1, + docCountFormatted: '', + viewableInLens: false, + }, + { + fieldName: 'type.keyword', + type: ML_JOB_FIELD_TYPES.KEYWORD, + existsInDocs: true, + aggregatable: true, + loading: false, + exampleCount: 1, + docCountFormatted: '5000 (100%)', + viewableInLens: true, + }, + ], + emptyFields: ['sourcetype'], + visibleMetricFieldsCount: 1, + totalMetricFieldsCount: 1, + populatedFieldsCount: 7, + totalFieldsCount: 8, + fieldNameFiltersResultCount: 1, + fieldTypeFiltersResultCount: 3, + }, +}; + +export const farequoteLuceneSearchTestData: TestData = { + suiteTitle: 'lucene saved search', + isSavedSearch: true, + sourceIndexOrSavedSearch: 'ft_farequote_lucene', + fieldNameFilters: ['@version.keyword', 'type'], + fieldTypeFilters: [ML_JOB_FIELD_TYPES.NUMBER], + sampleSizeValidations: [ + { size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } }, + { size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } }, + ], + expected: { + totalDocCountFormatted: '34,416', + metricFields: [ + { + fieldName: 'responsetime', + type: ML_JOB_FIELD_TYPES.NUMBER, + existsInDocs: true, + aggregatable: true, + loading: false, + docCountFormatted: '5000 (100%)', + statsMaxDecimalPlaces: 3, + topValuesCount: 10, + viewableInLens: true, + }, + ], + nonMetricFields: [ + { + fieldName: '@timestamp', + type: ML_JOB_FIELD_TYPES.DATE, + existsInDocs: true, + aggregatable: true, + loading: false, + docCountFormatted: '5000 (100%)', + exampleCount: 2, + viewableInLens: true, + }, + { + fieldName: '@version', + type: ML_JOB_FIELD_TYPES.TEXT, + existsInDocs: true, + aggregatable: false, + loading: false, + exampleCount: 1, + docCountFormatted: '', + viewableInLens: false, + }, + { + fieldName: '@version.keyword', + type: ML_JOB_FIELD_TYPES.KEYWORD, + existsInDocs: true, + aggregatable: true, + loading: false, + exampleCount: 1, + docCountFormatted: '5000 (100%)', + viewableInLens: true, + }, + { + fieldName: 'airline', + type: ML_JOB_FIELD_TYPES.KEYWORD, + existsInDocs: true, + aggregatable: true, + loading: false, + exampleCount: 5, + docCountFormatted: '5000 (100%)', + viewableInLens: true, + }, + { + fieldName: 'type', + type: ML_JOB_FIELD_TYPES.TEXT, + existsInDocs: true, + aggregatable: false, + loading: false, + exampleCount: 1, + docCountFormatted: '', + viewableInLens: false, + }, + { + fieldName: 'type.keyword', + type: ML_JOB_FIELD_TYPES.KEYWORD, + existsInDocs: true, + aggregatable: true, + loading: false, + exampleCount: 1, + docCountFormatted: '5000 (100%)', + viewableInLens: true, + }, + ], + emptyFields: ['sourcetype'], + visibleMetricFieldsCount: 1, + totalMetricFieldsCount: 1, + populatedFieldsCount: 7, + totalFieldsCount: 8, + fieldNameFiltersResultCount: 2, + fieldTypeFiltersResultCount: 1, + }, +}; + +export const farequoteLuceneFiltersSearchTestData: TestData = { + suiteTitle: 'lucene saved search and filter', + isSavedSearch: true, + sourceIndexOrSavedSearch: 'ft_farequote_filter_and_lucene', + fieldNameFilters: ['@version.keyword', 'type'], + fieldTypeFilters: [ML_JOB_FIELD_TYPES.NUMBER], + sampleSizeValidations: [ + { size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } }, + { size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } }, + ], + expected: { + totalDocCountFormatted: '5,673', + metricFields: [ + { + fieldName: 'responsetime', + type: ML_JOB_FIELD_TYPES.NUMBER, + existsInDocs: true, + aggregatable: true, + loading: false, + docCountFormatted: '5000 (100%)', + statsMaxDecimalPlaces: 3, + topValuesCount: 10, + viewableInLens: true, + }, + ], + nonMetricFields: [ + { + fieldName: '@timestamp', + type: ML_JOB_FIELD_TYPES.DATE, + existsInDocs: true, + aggregatable: true, + loading: false, + docCountFormatted: '5000 (100%)', + exampleCount: 2, + viewableInLens: true, + }, + { + fieldName: '@version', + type: ML_JOB_FIELD_TYPES.TEXT, + existsInDocs: true, + aggregatable: false, + loading: false, + exampleCount: 1, + docCountFormatted: '', + viewableInLens: false, + }, + { + fieldName: '@version.keyword', + type: ML_JOB_FIELD_TYPES.KEYWORD, + existsInDocs: true, + aggregatable: true, + loading: false, + exampleCount: 1, + docCountFormatted: '5000 (100%)', + viewableInLens: true, + }, + { + fieldName: 'airline', + type: ML_JOB_FIELD_TYPES.KEYWORD, + existsInDocs: true, + aggregatable: true, + loading: false, + exampleCount: 1, + exampleContent: ['ASA'], + docCountFormatted: '5000 (100%)', + viewableInLens: true, + }, + { + fieldName: 'type', + type: ML_JOB_FIELD_TYPES.TEXT, + existsInDocs: true, + aggregatable: false, + loading: false, + exampleCount: 1, + docCountFormatted: '', + viewableInLens: false, + }, + { + fieldName: 'type.keyword', + type: ML_JOB_FIELD_TYPES.KEYWORD, + existsInDocs: true, + aggregatable: true, + loading: false, + exampleCount: 1, + docCountFormatted: '5000 (100%)', + viewableInLens: true, + }, + ], + emptyFields: ['sourcetype'], + visibleMetricFieldsCount: 1, + totalMetricFieldsCount: 1, + populatedFieldsCount: 7, + totalFieldsCount: 8, + fieldNameFiltersResultCount: 2, + fieldTypeFiltersResultCount: 1, + }, +}; + +export const sampleLogTestData: TestData = { + suiteTitle: 'geo point field', + isSavedSearch: false, + sourceIndexOrSavedSearch: 'ft_module_sample_logs', + fieldNameFilters: ['geo.coordinates'], + fieldTypeFilters: [ML_JOB_FIELD_TYPES.GEO_POINT], + rowsPerPage: 50, + expected: { + totalDocCountFormatted: '408', + metricFields: [], + // only testing the geo_point fields + nonMetricFields: [ + { + fieldName: 'geo.coordinates', + type: ML_JOB_FIELD_TYPES.GEO_POINT, + existsInDocs: true, + aggregatable: true, + loading: false, + docCountFormatted: '408 (100%)', + exampleCount: 10, + viewableInLens: false, + }, + ], + emptyFields: [], + visibleMetricFieldsCount: 4, + totalMetricFieldsCount: 5, + populatedFieldsCount: 35, + totalFieldsCount: 36, + fieldNameFiltersResultCount: 1, + fieldTypeFiltersResultCount: 1, + }, + sampleSizeValidations: [ + { size: 1000, expected: { field: 'geo.coordinates', docCountFormatted: '408 (100%)' } }, + { size: 5000, expected: { field: '@timestamp', docCountFormatted: '408 (100%)' } }, + ], +}; diff --git a/x-pack/test/functional/apps/ml/data_visualizer/types.ts b/x-pack/test/functional/apps/ml/data_visualizer/types.ts new file mode 100644 index 0000000000000..5c3f890dba561 --- /dev/null +++ b/x-pack/test/functional/apps/ml/data_visualizer/types.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FieldVisConfig } from '../../../../../plugins/data_visualizer/public/application/common/components/stats_table/types'; + +export interface MetricFieldVisConfig extends FieldVisConfig { + statsMaxDecimalPlaces: number; + docCountFormatted: string; + topValuesCount: number; + viewableInLens: boolean; +} + +export interface NonMetricFieldVisConfig extends FieldVisConfig { + docCountFormatted: string; + exampleCount: number; + exampleContent?: string[]; + viewableInLens: boolean; +} + +export interface TestData { + suiteTitle: string; + isSavedSearch?: boolean; + sourceIndexOrSavedSearch: string; + fieldNameFilters: string[]; + fieldTypeFilters: string[]; + rowsPerPage?: 10 | 25 | 50; + sampleSizeValidations: Array<{ + size: number; + expected: { field: string; docCountFormatted: string }; + }>; + expected: { + totalDocCountFormatted: string; + metricFields?: MetricFieldVisConfig[]; + nonMetricFields?: NonMetricFieldVisConfig[]; + emptyFields: string[]; + visibleMetricFieldsCount: number; + totalMetricFieldsCount: number; + populatedFieldsCount: number; + totalFieldsCount: number; + fieldNameFiltersResultCount: number; + fieldTypeFiltersResultCount: number; + }; +} diff --git a/x-pack/test/functional/services/ml/custom_urls.ts b/x-pack/test/functional/services/ml/custom_urls.ts index 5b2bf0773719c..3d26236741a8a 100644 --- a/x-pack/test/functional/services/ml/custom_urls.ts +++ b/x-pack/test/functional/services/ml/custom_urls.ts @@ -169,7 +169,10 @@ export function MachineLearningCustomUrlsProvider({ async assertDiscoverCustomUrlAction(expectedHitCountFormatted: string) { await PageObjects.discover.waitForDiscoverAppOnScreen(); - await retry.tryForTime(5000, async () => { + // During cloud tests, the small browser width might cause hit count to be invisible + // so temporarily collapsing the sidebar ensures the count shows + await PageObjects.discover.closeSidebar(); + await retry.tryForTime(10 * 1000, async () => { const hitCount = await PageObjects.discover.getHitCount(); expect(hitCount).to.eql( expectedHitCountFormatted, diff --git a/x-pack/test/functional/services/ml/data_visualizer_table.ts b/x-pack/test/functional/services/ml/data_visualizer_table.ts index 8094f0ad1f8d2..860f2bd86bec7 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_table.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_table.ts @@ -361,7 +361,27 @@ export function MachineLearningDataVisualizerTableProvider( }); } - public async assertTopValuesContents(fieldName: string, expectedTopValuesCount: number) { + public async assertTopValuesContent(fieldName: string, expectedTopValues: string[]) { + const selector = this.detailsSelector(fieldName, 'dataVisualizerFieldDataTopValuesContent'); + const topValuesElement = await testSubjects.find(selector); + const topValuesBars = await topValuesElement.findAllByTestSubject( + 'dataVisualizerFieldDataTopValueBar' + ); + + const topValuesBarsValues = await Promise.all( + topValuesBars.map(async (bar) => { + const visibleText = await bar.getVisibleText(); + return visibleText ? visibleText.split('\n')[0] : undefined; + }) + ); + + expect(topValuesBarsValues).to.eql( + expectedTopValues, + `Expected top values for field '${fieldName}' to equal '${expectedTopValues}' (got '${topValuesBarsValues}')` + ); + } + + public async assertTopValuesCount(fieldName: string, expectedTopValuesCount: number) { const selector = this.detailsSelector(fieldName, 'dataVisualizerFieldDataTopValuesContent'); const topValuesElement = await testSubjects.find(selector); const topValuesBars = await topValuesElement.findAllByTestSubject( @@ -401,7 +421,7 @@ export function MachineLearningDataVisualizerTableProvider( await testSubjects.existOrFail( this.detailsSelector(fieldName, 'dataVisualizerFieldDataTopValues') ); - await this.assertTopValuesContents(fieldName, topValuesCount); + await this.assertTopValuesCount(fieldName, topValuesCount); if (checkDistributionPreviewExist) { await this.assertDistributionPreviewExist(fieldName); @@ -433,7 +453,8 @@ export function MachineLearningDataVisualizerTableProvider( public async assertKeywordFieldContents( fieldName: string, docCountFormatted: string, - topValuesCount: number + topValuesCount: number, + exampleContent?: string[] ) { await this.assertRowExists(fieldName); await this.assertFieldDocCount(fieldName, docCountFormatted); @@ -442,7 +463,11 @@ export function MachineLearningDataVisualizerTableProvider( await testSubjects.existOrFail( this.detailsSelector(fieldName, 'dataVisualizerFieldDataTopValuesContent') ); - await this.assertTopValuesContents(fieldName, topValuesCount); + await this.assertTopValuesCount(fieldName, topValuesCount); + + if (exampleContent) { + await this.assertTopValuesContent(fieldName, exampleContent); + } await this.ensureDetailsClosed(fieldName); } @@ -508,13 +533,19 @@ export function MachineLearningDataVisualizerTableProvider( docCountFormatted: string, exampleCount: number, viewableInLens: boolean, - hasActionMenu?: boolean + hasActionMenu?: boolean, + exampleContent?: string[] ) { // Currently the data used in the data visualizer tests only contains these field types. if (fieldType === ML_JOB_FIELD_TYPES.DATE) { await this.assertDateFieldContents(fieldName, docCountFormatted); } else if (fieldType === ML_JOB_FIELD_TYPES.KEYWORD) { - await this.assertKeywordFieldContents(fieldName, docCountFormatted, exampleCount); + await this.assertKeywordFieldContents( + fieldName, + docCountFormatted, + exampleCount, + exampleContent + ); } else if (fieldType === ML_JOB_FIELD_TYPES.TEXT) { await this.assertTextFieldContents(fieldName, docCountFormatted, exampleCount); } else if (fieldType === ML_JOB_FIELD_TYPES.GEO_POINT) { diff --git a/x-pack/test/functional_basic/apps/ml/data_visualizer/index.ts b/x-pack/test/functional_basic/apps/ml/data_visualizer/index.ts index 57a44a0b7952d..4d38e6a144a78 100644 --- a/x-pack/test/functional_basic/apps/ml/data_visualizer/index.ts +++ b/x-pack/test/functional_basic/apps/ml/data_visualizer/index.ts @@ -19,6 +19,11 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile( require.resolve('../../../../functional/apps/ml/data_visualizer/index_data_visualizer') ); + loadTestFile( + require.resolve( + '../../../../functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_discover' + ) + ); loadTestFile(require.resolve('./index_data_visualizer_actions_panel')); }); } From 5bdff367aa7d5d6a5935a09a8f38ff3922dd33f7 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 21 Oct 2021 18:41:50 +0100 Subject: [PATCH 20/40] fix(NA): creation of multiple processes on production by splitting no_transpilation when setting up node env (#115246) * fix(NA): adds no_transpilation_dist to avoid preserve_symlinks on dist * chore(NA): setup node env correctly on functional tests * chore(NA): try to fix tests * chore(NA): correctly separate split * chore(NA): check ensure preserve symlinks need * chore(NA): investigate path resolve result * chore(NA): investigate path resolve result #2 * chore(NA): comment out preserve symlinks * chore(NA): apply fs.realpathSync into the calculated REPO_ROOT paths on babel_register_for_test_plugins * chore(NA): removes debug code * chore(NA): move array definition * chore(NA): correctly import fs * chore(NA): add debug code * chore(NA): some more debug statements * chore(NA): remove ensure symlinks * chore(NA): trying to solve double symlinking * chore(NA): test mappings * chore(NA): process path * chore(NA): test a second map * chore(NA): using a different mappings * chore(NA): more debug cases * chore(NA): more debug logic * chore(NA): more debug cases * chore(NA): more debug cases * chore(NA): more debug cases * chore(NA): try to add realpathSync into require * chore(NA): try to add realpathSync into require * fix(NA): jenkins and buildkite run * chore(NA): add debug logs * chore(NA): correct path * chore(NA): correct path * chore(NA): add more test maps * chore(NA): add more test maps * chore(NA): add some more test maps experiments * chore(NA): try to remove another test map dep * chore(NA): try to remove another test map dep * chore(NA): try to remove another test map dep * chore(NA): try to remove another test map dep * chore(NA): try to remove another test map dep * chore(NA): try to remove another test map dep * chore(NA): try to remove another test map dep * chore(NA): try to remove another test map dep * chore(NA): include all correct transpilations for each jenkins path * chore(NA): include all correct transpilations for each used asset path * chore(NA): include all correct transpilations for each used asset path * chore(NA): remove jenkins support Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../lib/babel_register_for_test_plugins.js | 27 ++++++++++++------- src/setup_node_env/dist.js | 2 +- src/setup_node_env/no_transpilation.js | 10 +------ src/setup_node_env/no_transpilation_dist.js | 16 +++++++++++ 4 files changed, 35 insertions(+), 20 deletions(-) create mode 100644 src/setup_node_env/no_transpilation_dist.js diff --git a/packages/kbn-test/src/functional_tests/lib/babel_register_for_test_plugins.js b/packages/kbn-test/src/functional_tests/lib/babel_register_for_test_plugins.js index 2ded0e509c253..09ed81b62a09d 100644 --- a/packages/kbn-test/src/functional_tests/lib/babel_register_for_test_plugins.js +++ b/packages/kbn-test/src/functional_tests/lib/babel_register_for_test_plugins.js @@ -6,23 +6,30 @@ * Side Public License, v 1. */ +const Fs = require('fs'); const Path = require('path'); -const { REPO_ROOT } = require('@kbn/dev-utils'); +const { REPO_ROOT: REPO_ROOT_FOLLOWING_SYMLINKS } = require('@kbn/dev-utils'); +const BASE_REPO_ROOT = Path.resolve( + Fs.realpathSync(Path.resolve(REPO_ROOT_FOLLOWING_SYMLINKS, 'package.json')), + '..' +); + +const transpileKbnPaths = [ + 'test', + 'x-pack/test', + 'examples', + 'x-pack/examples', + // TODO: should should probably remove this link back to the source + 'x-pack/plugins/task_manager/server/config.ts', + 'src/core/utils/default_app_categories.ts', +].map((path) => Path.resolve(BASE_REPO_ROOT, path)); // modifies all future calls to require() to automatically // compile the required source with babel require('@babel/register')({ ignore: [/[\/\\](node_modules|target|dist)[\/\\]/], - only: [ - Path.resolve(REPO_ROOT, 'test'), - Path.resolve(REPO_ROOT, 'x-pack/test'), - Path.resolve(REPO_ROOT, 'examples'), - Path.resolve(REPO_ROOT, 'x-pack/examples'), - // TODO: should should probably remove this link back to the source - Path.resolve(REPO_ROOT, 'x-pack/plugins/task_manager/server/config.ts'), - Path.resolve(REPO_ROOT, 'src/core/utils/default_app_categories.ts'), - ], + only: transpileKbnPaths, babelrc: false, presets: [require.resolve('@kbn/babel-preset/node_preset')], extensions: ['.js', '.ts', '.tsx'], diff --git a/src/setup_node_env/dist.js b/src/setup_node_env/dist.js index 1d901b9ef5f06..3628a27a7793f 100644 --- a/src/setup_node_env/dist.js +++ b/src/setup_node_env/dist.js @@ -6,5 +6,5 @@ * Side Public License, v 1. */ -require('./no_transpilation'); +require('./no_transpilation_dist'); require('./polyfill'); diff --git a/src/setup_node_env/no_transpilation.js b/src/setup_node_env/no_transpilation.js index 1826f5bb0297d..b9497734b40bc 100644 --- a/src/setup_node_env/no_transpilation.js +++ b/src/setup_node_env/no_transpilation.js @@ -7,12 +7,4 @@ */ require('./ensure_node_preserve_symlinks'); - -// The following require statements MUST be executed before any others - BEGIN -require('./exit_on_warning'); -require('./harden'); -// The following require statements MUST be executed before any others - END - -require('symbol-observable'); -require('source-map-support/register'); -require('./node_version_validator'); +require('./no_transpilation_dist'); diff --git a/src/setup_node_env/no_transpilation_dist.js b/src/setup_node_env/no_transpilation_dist.js new file mode 100644 index 0000000000000..c52eba70f4ad3 --- /dev/null +++ b/src/setup_node_env/no_transpilation_dist.js @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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. + */ + +// The following require statements MUST be executed before any others - BEGIN +require('./exit_on_warning'); +require('./harden'); +// The following require statements MUST be executed before any others - END + +require('symbol-observable'); +require('source-map-support/register'); +require('./node_version_validator'); From e3aba08ea9893fbb8f813115fab1ca5c0fbcc068 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Thu, 21 Oct 2021 19:43:30 +0200 Subject: [PATCH 21/40] [Lens] Fix editor blowing up when working on non-exisiting data view (#114816) * correct styles for config panel (if data view is unavailable, the margins are still ok) * temp * fix data views bugs * add test * integrate feedback * Update datapanel.tsx * Update x-pack/plugins/lens/public/indexpattern_datasource/loader.ts Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../lens/public/app_plugin/mounter.tsx | 4 +- .../config_panel/layer_panel.scss | 3 +- .../change_indexpattern.tsx | 7 +- .../dimension_panel/dimension_editor.scss | 4 + .../dimension_panel/dimension_editor.tsx | 2 +- .../dimension_panel/dimension_panel.tsx | 4 +- .../droppable/get_drop_props.ts | 3 + .../indexpattern_datasource/indexpattern.tsx | 33 ++++--- .../indexpattern_suggestions.ts | 6 +- .../indexpattern_datasource/layerpanel.tsx | 1 + .../indexpattern_datasource/loader.test.ts | 91 +++++++++++++++++++ .../public/indexpattern_datasource/loader.ts | 85 +++++++++-------- .../time_shift_utils.tsx | 6 +- .../public/indexpattern_datasource/utils.ts | 2 +- .../lens/public/state_management/selectors.ts | 10 +- .../functional/apps/lens/error_handling.ts | 69 ++++++++++++++ x-pack/test/functional/apps/lens/index.ts | 1 + .../fixtures/kbn_archiver/lens/errors.json | 78 ++++++++++++++++ .../fixtures/kbn_archiver/lens/errors2.json | 16 ++++ .../test/functional/page_objects/lens_page.ts | 14 ++- 20 files changed, 369 insertions(+), 70 deletions(-) create mode 100644 x-pack/test/functional/apps/lens/error_handling.ts create mode 100644 x-pack/test/functional/fixtures/kbn_archiver/lens/errors.json create mode 100644 x-pack/test/functional/fixtures/kbn_archiver/lens/errors2.json diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 692fb0499176d..958f36d227cc6 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -40,7 +40,7 @@ import { LensAppState, LensState, } from '../state_management'; -import { getPreloadedState } from '../state_management/lens_slice'; +import { getPreloadedState, setState } from '../state_management/lens_slice'; import { getLensInspectorService } from '../lens_inspector_service'; export async function getLensServices( @@ -205,7 +205,7 @@ export async function mountApp( if (!initialContext) { data.query.filterManager.setAppFilters([]); } - + lensStore.dispatch(setState(emptyState)); lensStore.dispatch(loadInitial({ redirectCallback, initialInput, history: props.history })); return ( diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss index 781a08d0f60bb..4c699ff899bba 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss @@ -122,8 +122,7 @@ } .lnsLayerPanel__styleEditor { - margin-top: -$euiSizeS; - padding: 0 $euiSize $euiSize; + padding: $euiSize; } .lnsLayerPanel__colorIndicator { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx index ca44e833981ab..d5fabb9d7ef80 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx @@ -19,6 +19,7 @@ export type ChangeIndexPatternTriggerProps = ToolbarButtonProps & { export function ChangeIndexPattern({ indexPatternRefs, + isMissingCurrent, indexPatternId, onChangeIndexPattern, trigger, @@ -26,14 +27,13 @@ export function ChangeIndexPattern({ }: { trigger: ChangeIndexPatternTriggerProps; indexPatternRefs: IndexPatternRef[]; + isMissingCurrent?: boolean; onChangeIndexPattern: (newId: string) => void; indexPatternId?: string; selectableProps?: EuiSelectableProps; }) { const [isPopoverOpen, setPopoverIsOpen] = useState(false); - const isMissingCurrent = !indexPatternRefs.some(({ id }) => id === indexPatternId); - // be careful to only add color with a value, otherwise it will fallbacks to "primary" const colorProp = isMissingCurrent ? { @@ -61,6 +61,9 @@ export function ChangeIndexPattern({ setPopoverIsOpen(false)} display="block" diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.scss b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.scss index 30e2e00c7c85d..8b509e9c39b7b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.scss +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.scss @@ -26,6 +26,10 @@ padding: $euiSize; } +.lnsIndexPatternDimensionEditor__section--collapseNext { + margin-bottom: -$euiSizeL; +} + .lnsIndexPatternDimensionEditor__section--shaded { background-color: $euiColorLightestShade; border-bottom: $euiBorderThin; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index 93718c88b251c..74628a31ea281 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -758,7 +758,7 @@ export function DimensionEditor(props: DimensionEditorProps) { {TabContent} {!isFullscreen && !currentFieldIsInvalid && ( -
+
{!incompleteInfo && selectedColumn && temporaryState === 'none' && ( { const layer = state.layers[layerId]; + return !isColumnInvalid(layer, columnId, state.indexPatterns[layer.indexPatternId]); }, @@ -449,21 +450,23 @@ export function getIndexPatternDatasource({ } // Forward the indexpattern as well, as it is required by some operationType checks - const layerErrors = Object.entries(state.layers).map(([layerId, layer]) => - ( - getErrorMessages( - layer, - state.indexPatterns[layer.indexPatternId], - state, - layerId, - core - ) ?? [] - ).map((message) => ({ - shortMessage: '', // Not displayed currently - longMessage: typeof message === 'string' ? message : message.message, - fixAction: typeof message === 'object' ? message.fixAction : undefined, - })) - ); + const layerErrors = Object.entries(state.layers) + .filter(([_, layer]) => !!state.indexPatterns[layer.indexPatternId]) + .map(([layerId, layer]) => + ( + getErrorMessages( + layer, + state.indexPatterns[layer.indexPatternId], + state, + layerId, + core + ) ?? [] + ).map((message) => ({ + shortMessage: '', // Not displayed currently + longMessage: typeof message === 'string' ? message : message.message, + fixAction: typeof message === 'object' ? message.fixAction : undefined, + })) + ); // Single layer case, no need to explain more if (layerErrors.length <= 1) { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index 604b63aa29246..d3c292b7e019b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -406,7 +406,7 @@ export function getDatasourceSuggestionsFromCurrentState( layer.columns[columnId].isBucketed && layer.columns[columnId].dataType === 'date' ); const timeField = - indexPattern.timeFieldName && indexPattern.getFieldByName(indexPattern.timeFieldName); + indexPattern?.timeFieldName && indexPattern.getFieldByName(indexPattern.timeFieldName); const hasNumericDimension = buckets.length === 1 && @@ -428,7 +428,9 @@ export function getDatasourceSuggestionsFromCurrentState( // suggest current metric over time if there is a default time field suggestions.push(createSuggestionWithDefaultDateHistogram(state, layerId, timeField)); } - suggestions.push(...createAlternativeMetricSuggestions(indexPattern, layerId, state)); + if (indexPattern) { + suggestions.push(...createAlternativeMetricSuggestions(indexPattern, layerId, state)); + } } else { suggestions.push(...createSimplifiedTableSuggestions(state, layerId)); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx index 28f2921ccc771..27813846883b8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx @@ -40,6 +40,7 @@ export function LayerPanel({ state, layerId, onChangeIndexPattern }: IndexPatter }} indexPatternId={layer.indexPatternId} indexPatternRefs={state.indexPatternRefs} + isMissingCurrent={!indexPattern} onChangeIndexPattern={onChangeIndexPattern} /> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index e9cad4fc3a37e..d731069e6e7eb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -604,6 +604,97 @@ describe('loader', () => { indexPatternId: '2', }); }); + + it('should default to the first loaded index pattern if could not load any used one or one from the storage', async () => { + function mockIndexPatternsServiceWithConflict() { + return { + get: jest.fn(async (id: '1' | '2' | 'conflictId') => { + if (id === 'conflictId') { + return Promise.reject(new Error('Oh noes conflict boom')); + } + const result = { ...sampleIndexPatternsFromService[id], metaFields: [] }; + if (!result.fields) { + result.fields = []; + } + return result; + }), + getIdsWithTitle: jest.fn(async () => { + return [ + { + id: sampleIndexPatterns[1].id, + title: sampleIndexPatterns[1].title, + }, + { + id: sampleIndexPatterns[2].id, + title: sampleIndexPatterns[2].title, + }, + { + id: 'conflictId', + title: 'conflictId title', + }, + ]; + }), + } as unknown as Pick; + } + const savedState: IndexPatternPersistedState = { + layers: { + layerb: { + columnOrder: ['col1', 'col2'], + columns: { + col1: { + dataType: 'date', + isBucketed: true, + label: 'My date', + operationType: 'date_histogram', + params: { + interval: 'm', + }, + sourceField: 'timestamp', + }, + col2: { + dataType: 'number', + isBucketed: false, + label: 'Sum of bytes', + operationType: 'sum', + sourceField: 'bytes', + }, + }, + }, + }, + }; + const storage = createMockStorage({ indexPatternId: 'conflictId' }); + const state = await loadInitialState({ + persistedState: savedState, + references: [ + { + name: 'indexpattern-datasource-current-indexpattern', + id: 'conflictId', + type: 'index-pattern', + }, + { name: 'indexpattern-datasource-layer-layerb', id: 'conflictId', type: 'index-pattern' }, + ], + indexPatternsService: mockIndexPatternsServiceWithConflict(), + storage, + options: { isFullEditor: true }, + }); + + expect(state).toMatchObject({ + currentIndexPatternId: '1', + indexPatternRefs: [ + { id: 'conflictId', title: 'conflictId title' }, + { id: '1', title: sampleIndexPatterns['1'].title }, + { id: '2', title: sampleIndexPatterns['2'].title }, + ], + indexPatterns: { + '1': sampleIndexPatterns['1'], + }, + layers: { layerb: { ...savedState.layers.layerb, indexPatternId: 'conflictId' } }, + }); + + expect(storage.set).toHaveBeenCalledWith('lens-settings', { + indexPatternId: '1', + }); + }); }); describe('saved object references', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index ecd15732cf094..e1a15b87e5f5c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { uniq, mapValues } from 'lodash'; +import { uniq, mapValues, difference } from 'lodash'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { HttpSetup, SavedObjectReference } from 'kibana/public'; import { InitializationOptions, StateSetter } from '../types'; @@ -38,10 +38,12 @@ type ErrorHandler = (err: Error) => void; export async function loadIndexPatterns({ indexPatternsService, patterns, + notUsedPatterns, cache, }: { indexPatternsService: IndexPatternsService; patterns: string[]; + notUsedPatterns?: string[]; cache: Record; }) { const missingIds = patterns.filter((id) => !cache[id]); @@ -59,13 +61,23 @@ export async function loadIndexPatterns({ missingIds.map((id) => indexPatternsService.get(id)) ); // ignore rejected indexpatterns here, they're already handled at the app level - const indexPatterns = allIndexPatterns + let indexPatterns = allIndexPatterns .filter( (response): response is PromiseFulfilledResult => response.status === 'fulfilled' ) .map((response) => response.value); + // if all of the used index patterns failed to load, try loading one of not used ones till one succeeds + for (let i = 0; notUsedPatterns && i < notUsedPatterns?.length && !indexPatterns.length; i++) { + const resp = await indexPatternsService.get(notUsedPatterns[i]).catch((e) => { + // do nothing + }); + if (resp) { + indexPatterns = [resp]; + } + } + const indexPatternsObject = indexPatterns.reduce( (acc, indexPattern) => { const newFields = indexPattern.fields @@ -220,65 +232,62 @@ export async function loadInitialState({ const indexPatternRefs: IndexPatternRef[] = await (isFullEditor ? loadIndexPatternRefs(indexPatternsService) : []); + const lastUsedIndexPatternId = getLastUsedIndexPatternId(storage, indexPatternRefs); + const fallbackId = lastUsedIndexPatternId || defaultIndexPatternId || indexPatternRefs[0]?.id; const state = persistedState && references ? injectReferences(persistedState, references) : undefined; - - const fallbackId = lastUsedIndexPatternId || defaultIndexPatternId || indexPatternRefs[0]?.id; - - const requiredPatterns: string[] = uniq( - state - ? Object.values(state.layers) - .map((l) => l.indexPatternId) - .concat(state.currentIndexPatternId) - : [fallbackId] + const usedPatterns = ( + initialContext + ? [initialContext.indexPatternId] + : uniq( + state + ? Object.values(state.layers) + .map((l) => l.indexPatternId) + .concat(state.currentIndexPatternId) + : [fallbackId] + ) ) // take out the undefined from the list .filter(Boolean); + const notUsedPatterns: string[] = difference( + uniq(indexPatternRefs.map(({ id }) => id)), + usedPatterns + ); + const availableIndexPatterns = new Set(indexPatternRefs.map(({ id }: IndexPatternRef) => id)); + + const indexPatterns = await loadIndexPatterns({ + indexPatternsService, + cache: {}, + patterns: usedPatterns, + notUsedPatterns, + }); + // Priority list: // * start with the indexPattern in context - // * then fallback to the required ones - // * then as last resort use a random one from the available list + // * then fallback to the used ones + // * then as last resort use a first one from not used refs const availableIndexPatternIds = [ initialContext?.indexPatternId, - ...requiredPatterns, - indexPatternRefs[0]?.id, - ].filter((id) => id != null && availableIndexPatterns.has(id)); + ...usedPatterns, + ...notUsedPatterns, + ].filter((id) => id != null && availableIndexPatterns.has(id) && indexPatterns[id]); const currentIndexPatternId = availableIndexPatternIds[0]; if (currentIndexPatternId) { setLastUsedIndexPatternId(storage, currentIndexPatternId); - - if (!requiredPatterns.includes(currentIndexPatternId)) { - requiredPatterns.push(currentIndexPatternId); - } - } - - const indexPatterns = await loadIndexPatterns({ - indexPatternsService, - cache: {}, - patterns: initialContext ? [initialContext.indexPatternId] : requiredPatterns, - }); - if (state) { - return { - ...state, - currentIndexPatternId: currentIndexPatternId ?? fallbackId, - indexPatternRefs, - indexPatterns, - existingFields: {}, - isFirstExistenceFetch: true, - }; } return { - currentIndexPatternId: currentIndexPatternId ?? fallbackId, + layers: {}, + ...state, + currentIndexPatternId, indexPatternRefs, indexPatterns, - layers: {}, existingFields: {}, isFirstExistenceFetch: true, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx index 8cfd25914f59c..1258100375a39 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx @@ -150,9 +150,13 @@ export function getStateTimeShiftWarningMessages( if (!state) return; const warningMessages: React.ReactNode[] = []; Object.entries(state.layers).forEach(([layerId, layer]) => { + const layerIndexPattern = state.indexPatterns[layer.indexPatternId]; + if (!layerIndexPattern) { + return; + } const dateHistogramInterval = getDateHistogramInterval( layer, - state.indexPatterns[layer.indexPatternId], + layerIndexPattern, activeData, layerId ); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts index 7d225d730a757..a4e36367cef47 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts @@ -53,7 +53,7 @@ export function isColumnInvalid( indexPattern: IndexPattern ) { const column: IndexPatternColumn | undefined = layer.columns[columnId]; - if (!column) return; + if (!column || !indexPattern) return; const operationDefinition = column.operationType && operationDefinitionMap[column.operationType]; // check also references for errors diff --git a/x-pack/plugins/lens/public/state_management/selectors.ts b/x-pack/plugins/lens/public/state_management/selectors.ts index c1d1700d8b3b5..4b201e35e5cf7 100644 --- a/x-pack/plugins/lens/public/state_management/selectors.ts +++ b/x-pack/plugins/lens/public/state_management/selectors.ts @@ -146,8 +146,10 @@ export const selectDatasourceLayers = createSelector( export const selectFramePublicAPI = createSelector( [selectDatasourceStates, selectActiveData, selectDatasourceMap], - (datasourceStates, activeData, datasourceMap) => ({ - datasourceLayers: getDatasourceLayers(datasourceStates, datasourceMap), - activeData, - }) + (datasourceStates, activeData, datasourceMap) => { + return { + datasourceLayers: getDatasourceLayers(datasourceStates, datasourceMap), + activeData, + }; + } ); diff --git a/x-pack/test/functional/apps/lens/error_handling.ts b/x-pack/test/functional/apps/lens/error_handling.ts new file mode 100644 index 0000000000000..99263ddbc9bee --- /dev/null +++ b/x-pack/test/functional/apps/lens/error_handling.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects([ + 'visualize', + 'lens', + 'header', + 'timePicker', + 'common', + 'navigationalSearch', + ]); + const security = getService('security'); + const listingTable = getService('listingTable'); + const kibanaServer = getService('kibanaServer'); + + describe('Lens error handling', () => { + before(async () => { + await security.testUser.setRoles( + ['global_discover_read', 'global_visualize_read', 'test_logstash_reader'], + false + ); + // loading an object without reference fails, so we load data view + lens object and then unload data view + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/lens/errors' + ); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/lens/errors2' + ); + }); + + after(async () => { + await security.testUser.restoreDefaults(); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/lens/errors' + ); + }); + + describe('Index Pattern missing', () => { + it('the warning is shown and user can fix the state', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('lnsMetricWithNonExistingDataView'); + await PageObjects.lens.clickVisualizeListItemTitle('lnsMetricWithNonExistingDataView'); + await PageObjects.lens.waitForMissingDataViewWarning(); + await PageObjects.lens.switchToVisualization('lnsDatatable'); + await PageObjects.lens.waitForMissingDataViewWarning(); + await PageObjects.lens.switchToVisualization('donut'); + await PageObjects.lens.waitForMissingDataViewWarning(); + await PageObjects.lens.switchToVisualization('line'); + await PageObjects.lens.waitForMissingDataViewWarning(); + await PageObjects.lens.openDimensionEditor('lnsXY_yDimensionPanel > lns-dimensionTrigger'); + await PageObjects.lens.closeDimensionEditor(); + await PageObjects.lens.dragDimensionToDimension( + 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + 'lnsXY_yDimensionPanel > lns-empty-dimension' + ); + await PageObjects.lens.switchFirstLayerIndexPattern('log*'); + await PageObjects.lens.waitForMissingDataViewWarningDisappear(); + await PageObjects.lens.waitForEmptyWorkspace(); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts index 5241d9724abb9..86ceb4812ad3b 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/index.ts @@ -56,6 +56,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./heatmap')); loadTestFile(require.resolve('./reference_lines')); loadTestFile(require.resolve('./inspector')); + loadTestFile(require.resolve('./error_handling')); // has to be last one in the suite because it overrides saved objects loadTestFile(require.resolve('./rollup')); diff --git a/x-pack/test/functional/fixtures/kbn_archiver/lens/errors.json b/x-pack/test/functional/fixtures/kbn_archiver/lens/errors.json new file mode 100644 index 0000000000000..9ecc14164d863 --- /dev/null +++ b/x-pack/test/functional/fixtures/kbn_archiver/lens/errors.json @@ -0,0 +1,78 @@ +{ + "attributes": { + "fields": "[]", + "timeFieldName": "@timestamp", + "title": "nonExistingDataView" + }, + "coreMigrationVersion": "8.0.0", + "id": "nonExistingDataView", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2021-10-19T12:28:18.765Z", + "version": "WzU0ODUsMl0=" +} + +{ + "attributes": { + "description": "", + "state": { + "datasourceStates": { + "indexpattern": { + "layers": { + "eba8a330-0b65-46d4-8b1d-1528a0b53261": { + "columnOrder": [ + "eb55bd47-20ca-47fd-bf84-f72ac4b924ff" + ], + "columns": { + "eb55bd47-20ca-47fd-bf84-f72ac4b924ff": { + "dataType": "number", + "isBucketed": false, + "label": "Median of AvgTicketPrice", + "operationType": "median", + "scale": "ratio", + "sourceField": "AvgTicketPrice" + } + }, + "incompleteColumns": {} + } + } + } + }, + "filters": [], + "query": { + "language": "kuery", + "query": "" + }, + "visualization": { + "accessor": "eb55bd47-20ca-47fd-bf84-f72ac4b924ff", + "layerId": "eba8a330-0b65-46d4-8b1d-1528a0b53261", + "layerType": "data" + } + }, + "title": "lnsMetricWithNonExistingDataView", + "visualizationType": "lnsMetric" + }, + "coreMigrationVersion": "8.0.0", + "id": "3454af30-30e2-11ec-8dbc-f13e30d4f8ac", + "migrationVersion": { + "lens": "8.0.0" + }, + "references": [ + { + "id": "nonExistingDataView", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern" + }, + { + "id": "nonExistingDataView", + "name": "indexpattern-datasource-layer-eba8a330-0b65-46d4-8b1d-1528a0b53261", + "type": "index-pattern" + } + ], + "type": "lens", + "updated_at": "2021-10-19T13:41:04.038Z", + "version": "WzU2NjEsMl0=" +} \ No newline at end of file diff --git a/x-pack/test/functional/fixtures/kbn_archiver/lens/errors2.json b/x-pack/test/functional/fixtures/kbn_archiver/lens/errors2.json new file mode 100644 index 0000000000000..cfaafd51ad728 --- /dev/null +++ b/x-pack/test/functional/fixtures/kbn_archiver/lens/errors2.json @@ -0,0 +1,16 @@ +{ + "attributes": { + "fields": "[]", + "timeFieldName": "@timestamp", + "title": "nonExistingDataView" + }, + "coreMigrationVersion": "8.0.0", + "id": "nonExistingDataView", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2021-10-19T12:28:18.765Z", + "version": "WzU0ODUsMl0=" +} diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 01e860cf4bec5..790ac3ede496f 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -247,6 +247,18 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }); }, + async waitForMissingDataViewWarning() { + await retry.try(async () => { + await testSubjects.existOrFail(`missing-refs-failure`); + }); + }, + + async waitForMissingDataViewWarningDisappear() { + await retry.try(async () => { + await testSubjects.missingOrFail(`missing-refs-failure`); + }); + }, + async waitForEmptyWorkspace() { await retry.try(async () => { await testSubjects.existOrFail(`empty-workspace`); @@ -688,7 +700,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont */ async switchFirstLayerIndexPattern(name: string) { await testSubjects.click('lns_layerIndexPatternLabel'); - await find.clickByCssSelector(`[title="${name}"]`); + await find.clickByCssSelector(`.lnsChangeIndexPatternPopover [title="${name}"]`); await PageObjects.header.waitUntilLoadingHasFinished(); }, From b6c3762556c7de424646fb973631799cda1132a5 Mon Sep 17 00:00:00 2001 From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com> Date: Thu, 21 Oct 2021 19:44:41 +0200 Subject: [PATCH 22/40] [Fleet] remove old assets for manual upgrade too (#115926) * remove old assets for manual upgarde too * fixed tests * fixed tests --- .../services/epm/packages/cleanup.test.ts | 2 +- .../server/services/epm/packages/cleanup.ts | 2 +- .../server/services/package_policy.test.ts | 6 ++++++ .../fleet/server/services/package_policy.ts | 17 +++++++++++------ 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/epm/packages/cleanup.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/cleanup.test.ts index 482e42a46060e..07fd2d400b8d5 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/cleanup.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/cleanup.test.ts @@ -17,7 +17,7 @@ import { removeOldAssets } from './cleanup'; jest.mock('../..', () => ({ appContextService: { getLogger: () => ({ - info: jest.fn(), + debug: jest.fn(), }), }, })); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/cleanup.ts b/x-pack/plugins/fleet/server/services/epm/packages/cleanup.ts index d70beb53eddab..87eaa82aa85f0 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/cleanup.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/cleanup.ts @@ -57,7 +57,7 @@ async function removeAssetsFromVersion( if (total > 0) { appContextService .getLogger() - .info(`Package "${pkgName}-${oldVersion}" still being used by policies`); + .debug(`Package "${pkgName}-${oldVersion}" still being used by policies`); return; } diff --git a/x-pack/plugins/fleet/server/services/package_policy.test.ts b/x-pack/plugins/fleet/server/services/package_policy.test.ts index 9dc05ee2cb4ba..c25a1db753c73 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -128,6 +128,12 @@ jest.mock('./agent_policy', () => { }; }); +jest.mock('./epm/packages/cleanup', () => { + return { + removeOldAssets: jest.fn(), + }; +}); + const mockedFetchInfo = fetchInfo as jest.Mock>; type CombinedExternalCallback = PutPackagePolicyUpdateCallback | PostPackagePolicyCreateCallback; diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 9928ce3063159..fa9df22eb5e8c 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -424,7 +424,17 @@ class PackagePolicyService { user: options?.user, }); - return (await this.get(soClient, id)) as PackagePolicy; + const newPolicy = (await this.get(soClient, id)) as PackagePolicy; + + if (packagePolicy.package) { + await removeOldAssets({ + soClient, + pkgName: packagePolicy.package.name, + currentVersion: packagePolicy.package.version, + }); + } + + return newPolicy; } public async delete( @@ -596,11 +606,6 @@ class PackagePolicyService { name: packagePolicy.name, success: true, }); - await removeOldAssets({ - soClient, - pkgName: packageInfo.name, - currentVersion: packageInfo.version, - }); } catch (error) { // We only want to specifically handle validation errors for the new package policy. If a more severe or // general error is thrown elsewhere during the upgrade process, we want to surface that directly in From 60a8a89cd45e55b43fa1b18c7fc9d63616368c3d Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 21 Oct 2021 12:33:16 -0600 Subject: [PATCH 23/40] [Maps] fix locked tooltip issues (#115583) * [Maps] fix locked tooltip issues * rename cleanTooltipStateForLayer to updateTooltipStateForLayer * remove duplicated return * remove commented out line * review feedback Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/actions/data_request_actions.ts | 60 +++++++++++-------- .../maps/public/actions/layer_actions.ts | 6 +- .../maps/public/actions/map_actions.ts | 4 +- .../maps/public/actions/tooltip_actions.ts | 46 ++++++++------ .../features_tooltip/feature_properties.tsx | 9 ++- .../features_tooltip/features_tooltip.tsx | 14 ++++- .../tooltip_control/tooltip_popover.tsx | 40 ++++++++----- 7 files changed, 112 insertions(+), 67 deletions(-) diff --git a/x-pack/plugins/maps/public/actions/data_request_actions.ts b/x-pack/plugins/maps/public/actions/data_request_actions.ts index 48b0a416b5f0f..b912e8c52e680 100644 --- a/x-pack/plugins/maps/public/actions/data_request_actions.ts +++ b/x-pack/plugins/maps/public/actions/data_request_actions.ts @@ -32,7 +32,7 @@ import { getEventHandlers, ResultMeta, } from '../reducers/non_serializable_instances'; -import { cleanTooltipStateForLayer } from './tooltip_actions'; +import { updateTooltipStateForLayer } from './tooltip_actions'; import { LAYER_DATA_LOAD_ENDED, LAYER_DATA_LOAD_ERROR, @@ -61,7 +61,7 @@ export type DataRequestContext = { ): void; onLoadError(dataId: string, requestToken: symbol, errorMessage: string): void; onJoinError(errorMessage: string): void; - updateSourceData(newData: unknown): void; + updateSourceData(newData: object): void; isRequestStillActive(dataId: string, requestToken: symbol): boolean; registerCancelCallback(requestToken: symbol, callback: () => void): void; dataFilters: DataFilters; @@ -280,27 +280,30 @@ function endDataLoad( throw new DataRequestAbortError(); } - const features = data && 'features' in data ? (data as FeatureCollection).features : []; + if (dataId === SOURCE_DATA_REQUEST_ID) { + const features = data && 'features' in data ? (data as FeatureCollection).features : []; + + const eventHandlers = getEventHandlers(getState()); + if (eventHandlers && eventHandlers.onDataLoadEnd) { + const layer = getLayerById(layerId, getState()); + const resultMeta: ResultMeta = {}; + if (layer && layer.getType() === LAYER_TYPE.VECTOR) { + const featuresWithoutCentroids = features.filter((feature) => { + return feature.properties ? !feature.properties[KBN_IS_CENTROID_FEATURE] : true; + }); + resultMeta.featuresCount = featuresWithoutCentroids.length; + } - const eventHandlers = getEventHandlers(getState()); - if (eventHandlers && eventHandlers.onDataLoadEnd) { - const layer = getLayerById(layerId, getState()); - const resultMeta: ResultMeta = {}; - if (layer && layer.getType() === LAYER_TYPE.VECTOR) { - const featuresWithoutCentroids = features.filter((feature) => { - return feature.properties ? !feature.properties[KBN_IS_CENTROID_FEATURE] : true; + eventHandlers.onDataLoadEnd({ + layerId, + dataId, + resultMeta, }); - resultMeta.featuresCount = featuresWithoutCentroids.length; } - eventHandlers.onDataLoadEnd({ - layerId, - dataId, - resultMeta, - }); + dispatch(updateTooltipStateForLayer(layerId, features)); } - dispatch(cleanTooltipStateForLayer(layerId, features)); dispatch({ type: LAYER_DATA_LOAD_ENDED, layerId, @@ -331,16 +334,19 @@ function onDataLoadError( ) => { dispatch(unregisterCancelCallback(requestToken)); - const eventHandlers = getEventHandlers(getState()); - if (eventHandlers && eventHandlers.onDataLoadError) { - eventHandlers.onDataLoadError({ - layerId, - dataId, - errorMessage, - }); + if (dataId === SOURCE_DATA_REQUEST_ID) { + const eventHandlers = getEventHandlers(getState()); + if (eventHandlers && eventHandlers.onDataLoadError) { + eventHandlers.onDataLoadError({ + layerId, + dataId, + errorMessage, + }); + } + + dispatch(updateTooltipStateForLayer(layerId)); } - dispatch(cleanTooltipStateForLayer(layerId)); dispatch({ type: LAYER_DATA_LOAD_ERROR, layerId, @@ -361,6 +367,10 @@ export function updateSourceDataRequest(layerId: string, newData: object) { newData, }); + if ('features' in newData) { + dispatch(updateTooltipStateForLayer(layerId, (newData as FeatureCollection).features)); + } + dispatch(updateStyleMeta(layerId)); }; } diff --git a/x-pack/plugins/maps/public/actions/layer_actions.ts b/x-pack/plugins/maps/public/actions/layer_actions.ts index d67aef645b03a..9e937d86515e2 100644 --- a/x-pack/plugins/maps/public/actions/layer_actions.ts +++ b/x-pack/plugins/maps/public/actions/layer_actions.ts @@ -41,7 +41,7 @@ import { UPDATE_SOURCE_PROP, } from './map_action_constants'; import { clearDataRequests, syncDataForLayerId, updateStyleMeta } from './data_request_actions'; -import { cleanTooltipStateForLayer } from './tooltip_actions'; +import { updateTooltipStateForLayer } from './tooltip_actions'; import { Attribution, JoinDescriptor, @@ -217,7 +217,7 @@ export function setLayerVisibility(layerId: string, makeVisible: boolean) { } if (!makeVisible) { - dispatch(cleanTooltipStateForLayer(layerId)); + dispatch(updateTooltipStateForLayer(layerId)); } dispatch({ @@ -504,7 +504,7 @@ function removeLayerFromLayerList(layerId: string) { layerGettingRemoved.getInFlightRequestTokens().forEach((requestToken) => { dispatch(cancelRequest(requestToken)); }); - dispatch(cleanTooltipStateForLayer(layerId)); + dispatch(updateTooltipStateForLayer(layerId)); layerGettingRemoved.destroy(); dispatch({ type: REMOVE_LAYER, diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index ba52203ce486b..cf1e22ab90f88 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -60,7 +60,7 @@ import { addLayer, addLayerWithoutDataSync } from './layer_actions'; import { MapSettings } from '../reducers/map'; import { DrawState, MapCenterAndZoom, MapExtent, Timeslice } from '../../common/descriptor_types'; import { INITIAL_LOCATION } from '../../common/constants'; -import { cleanTooltipStateForLayer } from './tooltip_actions'; +import { updateTooltipStateForLayer } from './tooltip_actions'; import { VectorLayer } from '../classes/layers/vector_layer'; import { SET_DRAW_MODE } from './ui_actions'; import { expandToTileBoundaries } from '../../common/geo_tile_utils'; @@ -171,7 +171,7 @@ export function mapExtentChanged(mapExtentState: MapExtentState) { if (prevZoom !== nextZoom) { getLayerList(getState()).map((layer) => { if (!layer.showAtZoomLevel(nextZoom)) { - dispatch(cleanTooltipStateForLayer(layer.getId())); + dispatch(updateTooltipStateForLayer(layer.getId())); } }); } diff --git a/x-pack/plugins/maps/public/actions/tooltip_actions.ts b/x-pack/plugins/maps/public/actions/tooltip_actions.ts index c1b5f8190a73a..67b6842caeb46 100644 --- a/x-pack/plugins/maps/public/actions/tooltip_actions.ts +++ b/x-pack/plugins/maps/public/actions/tooltip_actions.ts @@ -10,8 +10,8 @@ import { Dispatch } from 'redux'; import { Feature } from 'geojson'; import { getOpenTooltips } from '../selectors/map_selectors'; import { SET_OPEN_TOOLTIPS } from './map_action_constants'; -import { FEATURE_ID_PROPERTY_NAME } from '../../common/constants'; -import { TooltipState } from '../../common/descriptor_types'; +import { FEATURE_ID_PROPERTY_NAME, FEATURE_VISIBLE_PROPERTY_NAME } from '../../common/constants'; +import { TooltipFeature, TooltipState } from '../../common/descriptor_types'; import { MapStoreState } from '../reducers/store'; export function closeOnClickTooltip(tooltipId: string) { @@ -62,26 +62,36 @@ export function openOnHoverTooltip(tooltipState: TooltipState) { }; } -export function cleanTooltipStateForLayer(layerId: string, layerFeatures: Feature[] = []) { +export function updateTooltipStateForLayer(layerId: string, layerFeatures: Feature[] = []) { return (dispatch: Dispatch, getState: () => MapStoreState) => { - let featuresRemoved = false; const openTooltips = getOpenTooltips(getState()) .map((tooltipState) => { - const nextFeatures = tooltipState.features.filter((tooltipFeature) => { + const nextFeatures: TooltipFeature[] = []; + tooltipState.features.forEach((tooltipFeature) => { if (tooltipFeature.layerId !== layerId) { // feature from another layer, keep it - return true; + nextFeatures.push(tooltipFeature); } - // Keep feature if it is still in layer - return layerFeatures.some((layerFeature) => { - return layerFeature.properties![FEATURE_ID_PROPERTY_NAME] === tooltipFeature.id; + const updatedFeature = layerFeatures.find((layerFeature) => { + const isVisible = + layerFeature.properties![FEATURE_VISIBLE_PROPERTY_NAME] !== undefined + ? layerFeature.properties![FEATURE_VISIBLE_PROPERTY_NAME] + : true; + return ( + isVisible && layerFeature.properties![FEATURE_ID_PROPERTY_NAME] === tooltipFeature.id + ); }); - }); - if (tooltipState.features.length !== nextFeatures.length) { - featuresRemoved = true; - } + if (updatedFeature) { + nextFeatures.push({ + ...tooltipFeature, + mbProperties: { + ...updatedFeature.properties, + }, + }); + } + }); return { ...tooltipState, features: nextFeatures }; }) @@ -89,11 +99,9 @@ export function cleanTooltipStateForLayer(layerId: string, layerFeatures: Featur return tooltipState.features.length > 0; }); - if (featuresRemoved) { - dispatch({ - type: SET_OPEN_TOOLTIPS, - openTooltips, - }); - } + dispatch({ + type: SET_OPEN_TOOLTIPS, + openTooltips, + }); }; } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/feature_properties.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/feature_properties.tsx index 4d9de61ffa819..570c06ff4ae7f 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/feature_properties.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/feature_properties.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import _ from 'lodash'; import React, { Component, CSSProperties, RefObject, ReactNode } from 'react'; import { EuiCallOut, @@ -57,6 +58,7 @@ export class FeatureProperties extends Component { private _isMounted = false; private _prevLayerId: string = ''; private _prevFeatureId?: string | number = ''; + private _prevMbProperties?: GeoJsonProperties; private readonly _tableRef: RefObject = React.createRef(); state: State = { @@ -118,13 +120,18 @@ export class FeatureProperties extends Component { nextFeatureId?: string | number; mbProperties: GeoJsonProperties; }) => { - if (this._prevLayerId === nextLayerId && this._prevFeatureId === nextFeatureId) { + if ( + this._prevLayerId === nextLayerId && + this._prevFeatureId === nextFeatureId && + _.isEqual(this._prevMbProperties, mbProperties) + ) { // do not reload same feature properties return; } this._prevLayerId = nextLayerId; this._prevFeatureId = nextFeatureId; + this._prevMbProperties = mbProperties; this.setState({ properties: null, loadPropertiesErrorMsg: null, diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/features_tooltip.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/features_tooltip.tsx index c0f792f626989..0d2ba07a5c956 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/features_tooltip.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/features_tooltip.tsx @@ -62,8 +62,20 @@ export class FeaturesTooltip extends Component { static getDerivedStateFromProps(nextProps: Props, prevState: State) { if (nextProps.features !== prevState.prevFeatures) { + let nextCurrentFeature = nextProps.features ? nextProps.features[0] : null; + if (prevState.currentFeature) { + const updatedCurrentFeature = nextProps.features.find((tooltipFeature) => { + return ( + tooltipFeature.id === prevState.currentFeature!.id && + tooltipFeature.layerId === prevState.currentFeature!.layerId + ); + }); + if (updatedCurrentFeature) { + nextCurrentFeature = updatedCurrentFeature; + } + } return { - currentFeature: nextProps.features ? nextProps.features[0] : null, + currentFeature: nextCurrentFeature, view: PROPERTIES_VIEW, prevFeatures: nextProps.features, }; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.tsx index 0b7ba3468d30c..181952a142ede 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.tsx @@ -44,15 +44,12 @@ interface Props { interface State { x?: number; y?: number; - isVisible: boolean; } export class TooltipPopover extends Component { private readonly _popoverRef: RefObject = React.createRef(); - state: State = { - isVisible: true, - }; + state: State = {}; componentDidMount() { this._updatePopoverPosition(); @@ -74,15 +71,19 @@ export class TooltipPopover extends Component { const lat = this.props.location[LAT_INDEX]; const lon = this.props.location[LON_INDEX]; const bounds = this.props.mbMap.getBounds(); - this.setState({ - x: nextPoint.x, - y: nextPoint.y, - isVisible: - lat < bounds.getNorth() && - lat > bounds.getSouth() && - lon > bounds.getWest() && - lon < bounds.getEast(), - }); + const isVisible = + lat < bounds.getNorth() && + lat > bounds.getSouth() && + lon > bounds.getWest() && + lon < bounds.getEast(); + if (!isVisible) { + this.props.closeTooltip(); + } else { + this.setState({ + x: nextPoint.x, + y: nextPoint.y, + }); + } }; _loadFeatureProperties = async ({ @@ -104,8 +105,15 @@ export class TooltipPopover extends Component { targetFeature = tooltipLayer.getFeatureById(featureId); } - const properties = targetFeature ? targetFeature.properties : mbProperties; - return await tooltipLayer.getPropertiesForTooltip(properties ? properties : {}); + let properties: GeoJsonProperties | undefined; + if (mbProperties) { + properties = mbProperties; + } else if (targetFeature?.properties) { + properties = targetFeature?.properties; + } else { + properties = {}; + } + return await tooltipLayer.getPropertiesForTooltip(properties); }; _getLayerName = async (layerId: string) => { @@ -143,7 +151,7 @@ export class TooltipPopover extends Component { }; render() { - if (!this.state.isVisible || this.state.x === undefined || this.state.y === undefined) { + if (this.state.x === undefined || this.state.y === undefined) { return null; } From 91b5c980d8006431df2a9562a2b6451ca653c798 Mon Sep 17 00:00:00 2001 From: Josh Dover <1813008+joshdover@users.noreply.github.com> Date: Thu, 21 Oct 2021 20:51:40 +0200 Subject: [PATCH 24/40] [Fleet] Update beats tutorial descriptions for unified integrations (#115829) Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: DeDe Morton --- .../services/sample_data/lib/register_with_integrations.ts | 2 +- src/plugins/home/server/tutorials/activemq_logs/index.ts | 4 ++-- .../home/server/tutorials/activemq_metrics/index.ts | 6 +++--- .../home/server/tutorials/aerospike_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/apache_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/apache_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/auditbeat/index.ts | 4 ++-- src/plugins/home/server/tutorials/auditd_logs/index.ts | 6 +++--- src/plugins/home/server/tutorials/aws_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/aws_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/azure_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/azure_metrics/index.ts | 4 ++-- src/plugins/home/server/tutorials/barracuda_logs/index.ts | 5 +++-- src/plugins/home/server/tutorials/bluecoat_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/cef_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/ceph_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/checkpoint_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/cisco_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/cloudwatch_logs/index.ts | 4 ++-- .../home/server/tutorials/cockroachdb_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/consul_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/coredns_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/coredns_metrics/index.ts | 6 +++--- .../home/server/tutorials/couchbase_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/couchdb_metrics/index.ts | 6 +++--- .../home/server/tutorials/crowdstrike_logs/index.ts | 5 +++-- src/plugins/home/server/tutorials/cylance_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/docker_metrics/index.ts | 6 +++--- .../home/server/tutorials/dropwizard_metrics/index.ts | 6 +++--- .../home/server/tutorials/elasticsearch_logs/index.ts | 4 ++-- .../home/server/tutorials/elasticsearch_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/envoyproxy_logs/index.ts | 4 ++-- .../home/server/tutorials/envoyproxy_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/etcd_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/f5_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/fortinet_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/gcp_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/gcp_metrics/index.ts | 7 +++---- src/plugins/home/server/tutorials/golang_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/gsuite_logs/index.ts | 6 +++--- src/plugins/home/server/tutorials/haproxy_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/haproxy_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/ibmmq_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/ibmmq_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/icinga_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/iis_logs/index.ts | 5 +++-- src/plugins/home/server/tutorials/iis_metrics/index.ts | 2 +- src/plugins/home/server/tutorials/imperva_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/infoblox_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/iptables_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/juniper_logs/index.ts | 2 +- src/plugins/home/server/tutorials/kafka_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/kafka_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/kibana_logs/index.ts | 2 +- src/plugins/home/server/tutorials/kibana_metrics/index.ts | 6 +++--- .../home/server/tutorials/kubernetes_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/logstash_logs/index.ts | 4 ++-- .../home/server/tutorials/logstash_metrics/index.ts | 6 +++--- .../home/server/tutorials/memcached_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/microsoft_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/misp_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/mongodb_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/mongodb_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/mssql_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/mssql_metrics/index.ts | 2 +- src/plugins/home/server/tutorials/munin_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/mysql_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/mysql_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/nats_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/nats_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/netflow_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/netscout_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/nginx_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/nginx_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/o365_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/okta_logs/index.ts | 4 ++-- .../home/server/tutorials/openmetrics_metrics/index.ts | 5 +++-- src/plugins/home/server/tutorials/oracle_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/osquery_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/panw_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/php_fpm_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/postgresql_logs/index.ts | 4 ++-- .../home/server/tutorials/postgresql_metrics/index.ts | 6 +++--- .../home/server/tutorials/prometheus_metrics/index.ts | 4 ++-- src/plugins/home/server/tutorials/rabbitmq_logs/index.ts | 4 ++-- .../home/server/tutorials/rabbitmq_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/radware_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/redis_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/redis_metrics/index.ts | 6 +++--- .../home/server/tutorials/redisenterprise_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/santa_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/sonicwall_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/sophos_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/squid_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/stan_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/statsd_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/suricata_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/system_logs/index.ts | 2 +- src/plugins/home/server/tutorials/system_metrics/index.ts | 7 ++++--- src/plugins/home/server/tutorials/tomcat_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/traefik_logs/index.ts | 4 ++-- src/plugins/home/server/tutorials/traefik_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/uptime_monitors/index.ts | 2 +- src/plugins/home/server/tutorials/uwsgi_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/vsphere_metrics/index.ts | 6 +++--- .../home/server/tutorials/windows_event_logs/index.ts | 6 +++--- src/plugins/home/server/tutorials/windows_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/zeek_logs/index.ts | 4 ++-- .../home/server/tutorials/zookeeper_metrics/index.ts | 6 +++--- src/plugins/home/server/tutorials/zscaler_logs/index.ts | 2 +- x-pack/plugins/apm/server/tutorial/index.ts | 2 +- x-pack/plugins/maps/server/tutorials/ems/index.ts | 2 +- 112 files changed, 265 insertions(+), 261 deletions(-) diff --git a/src/plugins/home/server/services/sample_data/lib/register_with_integrations.ts b/src/plugins/home/server/services/sample_data/lib/register_with_integrations.ts index e33cd58910fd6..d06dcacff18d9 100644 --- a/src/plugins/home/server/services/sample_data/lib/register_with_integrations.ts +++ b/src/plugins/home/server/services/sample_data/lib/register_with_integrations.ts @@ -22,7 +22,7 @@ export function registerSampleDatasetWithIntegration( defaultMessage: 'Sample Data', }), description: i18n.translate('home.sampleData.customIntegrationsDescription', { - defaultMessage: 'Add sample data and assets to Elasticsearch and Kibana.', + defaultMessage: 'Explore data in Kibana with these one-click data sets.', }), uiInternalPath: `${HOME_APP_BASE_PATH}#/tutorial_directory/sampleData`, isBeta: false, diff --git a/src/plugins/home/server/tutorials/activemq_logs/index.ts b/src/plugins/home/server/tutorials/activemq_logs/index.ts index 64a6fa575f5b6..a277b37838562 100644 --- a/src/plugins/home/server/tutorials/activemq_logs/index.ts +++ b/src/plugins/home/server/tutorials/activemq_logs/index.ts @@ -24,12 +24,12 @@ export function activemqLogsSpecProvider(context: TutorialContext): TutorialSche return { id: 'activemqLogs', name: i18n.translate('home.tutorials.activemqLogs.nameTitle', { - defaultMessage: 'ActiveMQ logs', + defaultMessage: 'ActiveMQ Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.activemqLogs.shortDescription', { - defaultMessage: 'Collect ActiveMQ logs with Filebeat.', + defaultMessage: 'Collect and parse logs from ActiveMQ instances with Filebeat.', }), longDescription: i18n.translate('home.tutorials.activemqLogs.longDescription', { defaultMessage: 'Collect ActiveMQ logs with Filebeat. \ diff --git a/src/plugins/home/server/tutorials/activemq_metrics/index.ts b/src/plugins/home/server/tutorials/activemq_metrics/index.ts index 7a59d6d4b70d1..9a001c149cda0 100644 --- a/src/plugins/home/server/tutorials/activemq_metrics/index.ts +++ b/src/plugins/home/server/tutorials/activemq_metrics/index.ts @@ -23,16 +23,16 @@ export function activemqMetricsSpecProvider(context: TutorialContext): TutorialS return { id: 'activemqMetrics', name: i18n.translate('home.tutorials.activemqMetrics.nameTitle', { - defaultMessage: 'ActiveMQ metrics', + defaultMessage: 'ActiveMQ Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.activemqMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from ActiveMQ instances.', + defaultMessage: 'Collect metrics from ActiveMQ instances with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.activemqMetrics.longDescription', { defaultMessage: - 'The `activemq` Metricbeat module fetches monitoring metrics from ActiveMQ instances \ + 'The `activemq` Metricbeat module fetches metrics from ActiveMQ instances \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-activemq.html', diff --git a/src/plugins/home/server/tutorials/aerospike_metrics/index.ts b/src/plugins/home/server/tutorials/aerospike_metrics/index.ts index 75dd45272db69..3e574f2c75496 100644 --- a/src/plugins/home/server/tutorials/aerospike_metrics/index.ts +++ b/src/plugins/home/server/tutorials/aerospike_metrics/index.ts @@ -23,17 +23,17 @@ export function aerospikeMetricsSpecProvider(context: TutorialContext): Tutorial return { id: 'aerospikeMetrics', name: i18n.translate('home.tutorials.aerospikeMetrics.nameTitle', { - defaultMessage: 'Aerospike metrics', + defaultMessage: 'Aerospike Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.aerospikeMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from the Aerospike server.', + defaultMessage: 'Collect metrics from Aerospike servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.aerospikeMetrics.longDescription', { defaultMessage: - 'The `aerospike` Metricbeat module fetches internal metrics from Aerospike. \ + 'The `aerospike` Metricbeat module fetches metrics from Aerospike. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-aerospike.html', diff --git a/src/plugins/home/server/tutorials/apache_logs/index.ts b/src/plugins/home/server/tutorials/apache_logs/index.ts index 8606a40fe0a23..6e588fd86588d 100644 --- a/src/plugins/home/server/tutorials/apache_logs/index.ts +++ b/src/plugins/home/server/tutorials/apache_logs/index.ts @@ -24,12 +24,12 @@ export function apacheLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'apacheLogs', name: i18n.translate('home.tutorials.apacheLogs.nameTitle', { - defaultMessage: 'Apache logs', + defaultMessage: 'Apache HTTP Server Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.apacheLogs.shortDescription', { - defaultMessage: 'Collect and parse access and error logs created by the Apache HTTP server.', + defaultMessage: 'Collect and parse logs from Apache HTTP servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.apacheLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/apache_metrics/index.ts b/src/plugins/home/server/tutorials/apache_metrics/index.ts index f013f3da737f0..17b495d1460c5 100644 --- a/src/plugins/home/server/tutorials/apache_metrics/index.ts +++ b/src/plugins/home/server/tutorials/apache_metrics/index.ts @@ -23,16 +23,16 @@ export function apacheMetricsSpecProvider(context: TutorialContext): TutorialSch return { id: 'apacheMetrics', name: i18n.translate('home.tutorials.apacheMetrics.nameTitle', { - defaultMessage: 'Apache metrics', + defaultMessage: 'Apache HTTP Server Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.apacheMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from the Apache 2 HTTP server.', + defaultMessage: 'Collect metrics from Apache HTTP servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.apacheMetrics.longDescription', { defaultMessage: - 'The `apache` Metricbeat module fetches internal metrics from the Apache 2 HTTP server. \ + 'The `apache` Metricbeat module fetches metrics from Apache 2 HTTP server. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-apache.html', diff --git a/src/plugins/home/server/tutorials/auditbeat/index.ts b/src/plugins/home/server/tutorials/auditbeat/index.ts index 8bd6450b1daa4..96e5d4bcda393 100644 --- a/src/plugins/home/server/tutorials/auditbeat/index.ts +++ b/src/plugins/home/server/tutorials/auditbeat/index.ts @@ -24,12 +24,12 @@ export function auditbeatSpecProvider(context: TutorialContext): TutorialSchema return { id: 'auditbeat', name: i18n.translate('home.tutorials.auditbeat.nameTitle', { - defaultMessage: 'Auditbeat', + defaultMessage: 'Auditbeat Events', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.auditbeat.shortDescription', { - defaultMessage: 'Collect audit data from your hosts.', + defaultMessage: 'Collect events from your servers with Auditbeat.', }), longDescription: i18n.translate('home.tutorials.auditbeat.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/auditd_logs/index.ts b/src/plugins/home/server/tutorials/auditd_logs/index.ts index a0d6f5f683e2c..6993196d93417 100644 --- a/src/plugins/home/server/tutorials/auditd_logs/index.ts +++ b/src/plugins/home/server/tutorials/auditd_logs/index.ts @@ -24,16 +24,16 @@ export function auditdLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'auditdLogs', name: i18n.translate('home.tutorials.auditdLogs.nameTitle', { - defaultMessage: 'Auditd logs', + defaultMessage: 'Auditd Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.auditdLogs.shortDescription', { - defaultMessage: 'Collect logs from the Linux auditd daemon.', + defaultMessage: 'Collect and parse logs from Linux audit daemon with Filebeat.', }), longDescription: i18n.translate('home.tutorials.auditdLogs.longDescription', { defaultMessage: - 'The module collects and parses logs from the audit daemon ( `auditd`). \ + 'The module collects and parses logs from audit daemon ( `auditd`). \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-auditd.html', diff --git a/src/plugins/home/server/tutorials/aws_logs/index.ts b/src/plugins/home/server/tutorials/aws_logs/index.ts index 3458800b33f0a..62fbcc4eebc18 100644 --- a/src/plugins/home/server/tutorials/aws_logs/index.ts +++ b/src/plugins/home/server/tutorials/aws_logs/index.ts @@ -24,12 +24,12 @@ export function awsLogsSpecProvider(context: TutorialContext): TutorialSchema { return { id: 'awsLogs', name: i18n.translate('home.tutorials.awsLogs.nameTitle', { - defaultMessage: 'AWS S3 based logs', + defaultMessage: 'AWS S3 based Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.awsLogs.shortDescription', { - defaultMessage: 'Collect AWS logs from S3 bucket with Filebeat.', + defaultMessage: 'Collect and parse logs from AWS S3 buckets with Filebeat.', }), longDescription: i18n.translate('home.tutorials.awsLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/aws_metrics/index.ts b/src/plugins/home/server/tutorials/aws_metrics/index.ts index 7c3a15a47d784..6bf1bf64bff9f 100644 --- a/src/plugins/home/server/tutorials/aws_metrics/index.ts +++ b/src/plugins/home/server/tutorials/aws_metrics/index.ts @@ -23,17 +23,17 @@ export function awsMetricsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'awsMetrics', name: i18n.translate('home.tutorials.awsMetrics.nameTitle', { - defaultMessage: 'AWS metrics', + defaultMessage: 'AWS Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.awsMetrics.shortDescription', { defaultMessage: - 'Fetch monitoring metrics for EC2 instances from the AWS APIs and Cloudwatch.', + 'Collect metrics for EC2 instances from AWS APIs and Cloudwatch with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.awsMetrics.longDescription', { defaultMessage: - 'The `aws` Metricbeat module fetches monitoring metrics from the AWS APIs and Cloudwatch. \ + 'The `aws` Metricbeat module fetches metrics from AWS APIs and Cloudwatch. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-aws.html', diff --git a/src/plugins/home/server/tutorials/azure_logs/index.ts b/src/plugins/home/server/tutorials/azure_logs/index.ts index 2bf1527a79c40..3c9438d9a6298 100644 --- a/src/plugins/home/server/tutorials/azure_logs/index.ts +++ b/src/plugins/home/server/tutorials/azure_logs/index.ts @@ -24,13 +24,13 @@ export function azureLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'azureLogs', name: i18n.translate('home.tutorials.azureLogs.nameTitle', { - defaultMessage: 'Azure logs', + defaultMessage: 'Azure Logs', }), moduleName, isBeta: true, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.azureLogs.shortDescription', { - defaultMessage: 'Collects Azure activity and audit related logs.', + defaultMessage: 'Collect and parse logs from Azure with Filebeat.', }), longDescription: i18n.translate('home.tutorials.azureLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/azure_metrics/index.ts b/src/plugins/home/server/tutorials/azure_metrics/index.ts index 4a6112510b333..310f954104634 100644 --- a/src/plugins/home/server/tutorials/azure_metrics/index.ts +++ b/src/plugins/home/server/tutorials/azure_metrics/index.ts @@ -23,13 +23,13 @@ export function azureMetricsSpecProvider(context: TutorialContext): TutorialSche return { id: 'azureMetrics', name: i18n.translate('home.tutorials.azureMetrics.nameTitle', { - defaultMessage: 'Azure metrics', + defaultMessage: 'Azure Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.azureMetrics.shortDescription', { - defaultMessage: 'Fetch Azure Monitor metrics.', + defaultMessage: 'Collect metrics from Azure with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.azureMetrics.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/barracuda_logs/index.ts b/src/plugins/home/server/tutorials/barracuda_logs/index.ts index 35ce10e00892e..cdfd75b9728b9 100644 --- a/src/plugins/home/server/tutorials/barracuda_logs/index.ts +++ b/src/plugins/home/server/tutorials/barracuda_logs/index.ts @@ -24,12 +24,13 @@ export function barracudaLogsSpecProvider(context: TutorialContext): TutorialSch return { id: 'barracudaLogs', name: i18n.translate('home.tutorials.barracudaLogs.nameTitle', { - defaultMessage: 'Barracuda logs', + defaultMessage: 'Barracuda Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.barracudaLogs.shortDescription', { - defaultMessage: 'Collect Barracuda Web Application Firewall logs over syslog or from a file.', + defaultMessage: + 'Collect and parse logs from Barracuda Web Application Firewall with Filebeat.', }), longDescription: i18n.translate('home.tutorials.barracudaLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/bluecoat_logs/index.ts b/src/plugins/home/server/tutorials/bluecoat_logs/index.ts index 85c7dff85d3e6..a7db5b04ee40d 100644 --- a/src/plugins/home/server/tutorials/bluecoat_logs/index.ts +++ b/src/plugins/home/server/tutorials/bluecoat_logs/index.ts @@ -24,12 +24,12 @@ export function bluecoatLogsSpecProvider(context: TutorialContext): TutorialSche return { id: 'bluecoatLogs', name: i18n.translate('home.tutorials.bluecoatLogs.nameTitle', { - defaultMessage: 'Bluecoat logs', + defaultMessage: 'Bluecoat Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.bluecoatLogs.shortDescription', { - defaultMessage: 'Collect Blue Coat Director logs over syslog or from a file.', + defaultMessage: 'Collect and parse logs from Blue Coat Director with Filebeat.', }), longDescription: i18n.translate('home.tutorials.bluecoatLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/cef_logs/index.ts b/src/plugins/home/server/tutorials/cef_logs/index.ts index cfd267f661d2a..1366198d610d7 100644 --- a/src/plugins/home/server/tutorials/cef_logs/index.ts +++ b/src/plugins/home/server/tutorials/cef_logs/index.ts @@ -24,12 +24,12 @@ export function cefLogsSpecProvider(context: TutorialContext): TutorialSchema { return { id: 'cefLogs', name: i18n.translate('home.tutorials.cefLogs.nameTitle', { - defaultMessage: 'CEF logs', + defaultMessage: 'CEF Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.cefLogs.shortDescription', { - defaultMessage: 'Collect Common Event Format (CEF) log data over syslog.', + defaultMessage: 'Collect and parse logs from Common Event Format (CEF) with Filebeat.', }), longDescription: i18n.translate('home.tutorials.cefLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/ceph_metrics/index.ts b/src/plugins/home/server/tutorials/ceph_metrics/index.ts index 821067d87c905..6a53789d26f7c 100644 --- a/src/plugins/home/server/tutorials/ceph_metrics/index.ts +++ b/src/plugins/home/server/tutorials/ceph_metrics/index.ts @@ -23,17 +23,17 @@ export function cephMetricsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'cephMetrics', name: i18n.translate('home.tutorials.cephMetrics.nameTitle', { - defaultMessage: 'Ceph metrics', + defaultMessage: 'Ceph Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.cephMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from the Ceph server.', + defaultMessage: 'Collect metrics from Ceph servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.cephMetrics.longDescription', { defaultMessage: - 'The `ceph` Metricbeat module fetches internal metrics from Ceph. \ + 'The `ceph` Metricbeat module fetches metrics from Ceph. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-ceph.html', diff --git a/src/plugins/home/server/tutorials/checkpoint_logs/index.ts b/src/plugins/home/server/tutorials/checkpoint_logs/index.ts index 9c0d5591ae35b..b5ea6be42403b 100644 --- a/src/plugins/home/server/tutorials/checkpoint_logs/index.ts +++ b/src/plugins/home/server/tutorials/checkpoint_logs/index.ts @@ -24,12 +24,12 @@ export function checkpointLogsSpecProvider(context: TutorialContext): TutorialSc return { id: 'checkpointLogs', name: i18n.translate('home.tutorials.checkpointLogs.nameTitle', { - defaultMessage: 'Check Point logs', + defaultMessage: 'Check Point Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.checkpointLogs.shortDescription', { - defaultMessage: 'Collect Check Point firewall logs.', + defaultMessage: 'Collect and parse logs from Check Point firewalls with Filebeat.', }), longDescription: i18n.translate('home.tutorials.checkpointLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/cisco_logs/index.ts b/src/plugins/home/server/tutorials/cisco_logs/index.ts index 50b79f448b316..922cfbf1e23ee 100644 --- a/src/plugins/home/server/tutorials/cisco_logs/index.ts +++ b/src/plugins/home/server/tutorials/cisco_logs/index.ts @@ -24,12 +24,12 @@ export function ciscoLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'ciscoLogs', name: i18n.translate('home.tutorials.ciscoLogs.nameTitle', { - defaultMessage: 'Cisco logs', + defaultMessage: 'Cisco Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.ciscoLogs.shortDescription', { - defaultMessage: 'Collect Cisco network device logs over syslog or from a file.', + defaultMessage: 'Collect and parse logs from Cisco network devices with Filebeat.', }), longDescription: i18n.translate('home.tutorials.ciscoLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts b/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts index cf0c27ed9be73..5564d11be4d19 100644 --- a/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts +++ b/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts @@ -23,12 +23,12 @@ export function cloudwatchLogsSpecProvider(context: TutorialContext): TutorialSc return { id: 'cloudwatchLogs', name: i18n.translate('home.tutorials.cloudwatchLogs.nameTitle', { - defaultMessage: 'AWS Cloudwatch logs', + defaultMessage: 'AWS Cloudwatch Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.cloudwatchLogs.shortDescription', { - defaultMessage: 'Collect Cloudwatch logs with Functionbeat.', + defaultMessage: 'Collect and parse logs from AWS Cloudwatch with Functionbeat.', }), longDescription: i18n.translate('home.tutorials.cloudwatchLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts b/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts index e43d05a0a098f..535c8aaa90768 100644 --- a/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts +++ b/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts @@ -23,16 +23,16 @@ export function cockroachdbMetricsSpecProvider(context: TutorialContext): Tutori return { id: 'cockroachdbMetrics', name: i18n.translate('home.tutorials.cockroachdbMetrics.nameTitle', { - defaultMessage: 'CockroachDB metrics', + defaultMessage: 'CockroachDB Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.cockroachdbMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from the CockroachDB server.', + defaultMessage: 'Collect metrics from CockroachDB servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.cockroachdbMetrics.longDescription', { defaultMessage: - 'The `cockroachdb` Metricbeat module fetches monitoring metrics from CockroachDB. \ + 'The `cockroachdb` Metricbeat module fetches metrics from CockroachDB. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-cockroachdb.html', diff --git a/src/plugins/home/server/tutorials/consul_metrics/index.ts b/src/plugins/home/server/tutorials/consul_metrics/index.ts index 915920db5882c..ca7179d55fd89 100644 --- a/src/plugins/home/server/tutorials/consul_metrics/index.ts +++ b/src/plugins/home/server/tutorials/consul_metrics/index.ts @@ -23,16 +23,16 @@ export function consulMetricsSpecProvider(context: TutorialContext): TutorialSch return { id: 'consulMetrics', name: i18n.translate('home.tutorials.consulMetrics.nameTitle', { - defaultMessage: 'Consul metrics', + defaultMessage: 'Consul Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.consulMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from the Consul server.', + defaultMessage: 'Collect metrics from Consul servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.consulMetrics.longDescription', { defaultMessage: - 'The `consul` Metricbeat module fetches monitoring metrics from Consul. \ + 'The `consul` Metricbeat module fetches metrics from Consul. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-consul.html', diff --git a/src/plugins/home/server/tutorials/coredns_logs/index.ts b/src/plugins/home/server/tutorials/coredns_logs/index.ts index 298464651f7fc..1261c67135001 100644 --- a/src/plugins/home/server/tutorials/coredns_logs/index.ts +++ b/src/plugins/home/server/tutorials/coredns_logs/index.ts @@ -24,12 +24,12 @@ export function corednsLogsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'corednsLogs', name: i18n.translate('home.tutorials.corednsLogs.nameTitle', { - defaultMessage: 'CoreDNS logs', + defaultMessage: 'CoreDNS Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.corednsLogs.shortDescription', { - defaultMessage: 'Collect CoreDNS logs.', + defaultMessage: 'Collect and parse logs from CoreDNS servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.corednsLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/coredns_metrics/index.ts b/src/plugins/home/server/tutorials/coredns_metrics/index.ts index 34912efb31a81..3abc14314a6ba 100644 --- a/src/plugins/home/server/tutorials/coredns_metrics/index.ts +++ b/src/plugins/home/server/tutorials/coredns_metrics/index.ts @@ -23,16 +23,16 @@ export function corednsMetricsSpecProvider(context: TutorialContext): TutorialSc return { id: 'corednsMetrics', name: i18n.translate('home.tutorials.corednsMetrics.nameTitle', { - defaultMessage: 'CoreDNS metrics', + defaultMessage: 'CoreDNS Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.corednsMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from the CoreDNS server.', + defaultMessage: 'Collect metrics from CoreDNS servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.corednsMetrics.longDescription', { defaultMessage: - 'The `coredns` Metricbeat module fetches monitoring metrics from CoreDNS. \ + 'The `coredns` Metricbeat module fetches metrics from CoreDNS. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-coredns.html', diff --git a/src/plugins/home/server/tutorials/couchbase_metrics/index.ts b/src/plugins/home/server/tutorials/couchbase_metrics/index.ts index 1860991fd17b2..5c29aa2d9a524 100644 --- a/src/plugins/home/server/tutorials/couchbase_metrics/index.ts +++ b/src/plugins/home/server/tutorials/couchbase_metrics/index.ts @@ -23,17 +23,17 @@ export function couchbaseMetricsSpecProvider(context: TutorialContext): Tutorial return { id: 'couchbaseMetrics', name: i18n.translate('home.tutorials.couchbaseMetrics.nameTitle', { - defaultMessage: 'Couchbase metrics', + defaultMessage: 'Couchbase Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.couchbaseMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from Couchbase.', + defaultMessage: 'Collect metrics from Couchbase databases with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.couchbaseMetrics.longDescription', { defaultMessage: - 'The `couchbase` Metricbeat module fetches internal metrics from Couchbase. \ + 'The `couchbase` Metricbeat module fetches metrics from Couchbase. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-couchbase.html', diff --git a/src/plugins/home/server/tutorials/couchdb_metrics/index.ts b/src/plugins/home/server/tutorials/couchdb_metrics/index.ts index a6c57f56cf2e1..00bea11d13d99 100644 --- a/src/plugins/home/server/tutorials/couchdb_metrics/index.ts +++ b/src/plugins/home/server/tutorials/couchdb_metrics/index.ts @@ -23,16 +23,16 @@ export function couchdbMetricsSpecProvider(context: TutorialContext): TutorialSc return { id: 'couchdbMetrics', name: i18n.translate('home.tutorials.couchdbMetrics.nameTitle', { - defaultMessage: 'CouchDB metrics', + defaultMessage: 'CouchDB Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.couchdbMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from the CouchdB server.', + defaultMessage: 'Collect metrics from CouchDB servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.couchdbMetrics.longDescription', { defaultMessage: - 'The `couchdb` Metricbeat module fetches monitoring metrics from CouchDB. \ + 'The `couchdb` Metricbeat module fetches metrics from CouchDB. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-couchdb.html', diff --git a/src/plugins/home/server/tutorials/crowdstrike_logs/index.ts b/src/plugins/home/server/tutorials/crowdstrike_logs/index.ts index baaaef50a641f..a48ed4288210b 100644 --- a/src/plugins/home/server/tutorials/crowdstrike_logs/index.ts +++ b/src/plugins/home/server/tutorials/crowdstrike_logs/index.ts @@ -24,12 +24,13 @@ export function crowdstrikeLogsSpecProvider(context: TutorialContext): TutorialS return { id: 'crowdstrikeLogs', name: i18n.translate('home.tutorials.crowdstrikeLogs.nameTitle', { - defaultMessage: 'CrowdStrike logs', + defaultMessage: 'CrowdStrike Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.crowdstrikeLogs.shortDescription', { - defaultMessage: 'Collect CrowdStrike Falcon logs using the Falcon SIEM Connector.', + defaultMessage: + 'Collect and parse logs from CrowdStrike Falcon using the Falcon SIEM Connector with Filebeat.', }), longDescription: i18n.translate('home.tutorials.crowdstrikeLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/cylance_logs/index.ts b/src/plugins/home/server/tutorials/cylance_logs/index.ts index 9766f417b8870..64b79a41cd2e0 100644 --- a/src/plugins/home/server/tutorials/cylance_logs/index.ts +++ b/src/plugins/home/server/tutorials/cylance_logs/index.ts @@ -24,12 +24,12 @@ export function cylanceLogsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'cylanceLogs', name: i18n.translate('home.tutorials.cylanceLogs.nameTitle', { - defaultMessage: 'CylancePROTECT logs', + defaultMessage: 'CylancePROTECT Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.cylanceLogs.shortDescription', { - defaultMessage: 'Collect CylancePROTECT logs over syslog or from a file.', + defaultMessage: 'Collect and parse logs from CylancePROTECT with Filebeat.', }), longDescription: i18n.translate('home.tutorials.cylanceLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/docker_metrics/index.ts b/src/plugins/home/server/tutorials/docker_metrics/index.ts index 6a8687ef5d66e..ab80e6d644dbc 100644 --- a/src/plugins/home/server/tutorials/docker_metrics/index.ts +++ b/src/plugins/home/server/tutorials/docker_metrics/index.ts @@ -23,16 +23,16 @@ export function dockerMetricsSpecProvider(context: TutorialContext): TutorialSch return { id: 'dockerMetrics', name: i18n.translate('home.tutorials.dockerMetrics.nameTitle', { - defaultMessage: 'Docker metrics', + defaultMessage: 'Docker Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.dockerMetrics.shortDescription', { - defaultMessage: 'Fetch metrics about your Docker containers.', + defaultMessage: 'Collect metrics from Docker containers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.dockerMetrics.longDescription', { defaultMessage: - 'The `docker` Metricbeat module fetches metrics from the Docker server. \ + 'The `docker` Metricbeat module fetches metrics from Docker server. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-docker.html', diff --git a/src/plugins/home/server/tutorials/dropwizard_metrics/index.ts b/src/plugins/home/server/tutorials/dropwizard_metrics/index.ts index 86be26dd12ca7..9864d376966bb 100644 --- a/src/plugins/home/server/tutorials/dropwizard_metrics/index.ts +++ b/src/plugins/home/server/tutorials/dropwizard_metrics/index.ts @@ -23,17 +23,17 @@ export function dropwizardMetricsSpecProvider(context: TutorialContext): Tutoria return { id: 'dropwizardMetrics', name: i18n.translate('home.tutorials.dropwizardMetrics.nameTitle', { - defaultMessage: 'Dropwizard metrics', + defaultMessage: 'Dropwizard Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.dropwizardMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from Dropwizard Java application.', + defaultMessage: 'Collect metrics from Dropwizard Java applciations with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.dropwizardMetrics.longDescription', { defaultMessage: - 'The `dropwizard` Metricbeat module fetches internal metrics from Dropwizard Java Application. \ + 'The `dropwizard` Metricbeat module fetches metrics from Dropwizard Java Application. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-dropwizard.html', diff --git a/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts b/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts index 1886a912fdcd2..6415781d02c06 100644 --- a/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts +++ b/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts @@ -24,13 +24,13 @@ export function elasticsearchLogsSpecProvider(context: TutorialContext): Tutoria return { id: 'elasticsearchLogs', name: i18n.translate('home.tutorials.elasticsearchLogs.nameTitle', { - defaultMessage: 'Elasticsearch logs', + defaultMessage: 'Elasticsearch Logs', }), moduleName, category: TutorialsCategory.LOGGING, isBeta: true, shortDescription: i18n.translate('home.tutorials.elasticsearchLogs.shortDescription', { - defaultMessage: 'Collect and parse logs created by Elasticsearch.', + defaultMessage: 'Collect and parse logs from Elasticsearch clusters with Filebeat.', }), longDescription: i18n.translate('home.tutorials.elasticsearchLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/elasticsearch_metrics/index.ts b/src/plugins/home/server/tutorials/elasticsearch_metrics/index.ts index 2adc2fd90fa70..3961d7f78c86c 100644 --- a/src/plugins/home/server/tutorials/elasticsearch_metrics/index.ts +++ b/src/plugins/home/server/tutorials/elasticsearch_metrics/index.ts @@ -23,17 +23,17 @@ export function elasticsearchMetricsSpecProvider(context: TutorialContext): Tuto return { id: 'elasticsearchMetrics', name: i18n.translate('home.tutorials.elasticsearchMetrics.nameTitle', { - defaultMessage: 'Elasticsearch metrics', + defaultMessage: 'Elasticsearch Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.elasticsearchMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from Elasticsearch.', + defaultMessage: 'Collect metrics from Elasticsearch clusters with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.elasticsearchMetrics.longDescription', { defaultMessage: - 'The `elasticsearch` Metricbeat module fetches internal metrics from Elasticsearch. \ + 'The `elasticsearch` Metricbeat module fetches metrics from Elasticsearch. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-elasticsearch.html', diff --git a/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts b/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts index fda69a2467b25..55c85a5bdd2a4 100644 --- a/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts +++ b/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts @@ -24,12 +24,12 @@ export function envoyproxyLogsSpecProvider(context: TutorialContext): TutorialSc return { id: 'envoyproxyLogs', name: i18n.translate('home.tutorials.envoyproxyLogs.nameTitle', { - defaultMessage: 'Envoy Proxy logs', + defaultMessage: 'Envoy Proxy Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.envoyproxyLogs.shortDescription', { - defaultMessage: 'Collect Envoy Proxy logs.', + defaultMessage: 'Collect and parse logs from Envoy Proxy with Filebeat.', }), longDescription: i18n.translate('home.tutorials.envoyproxyLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts b/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts index 263d1a2036fd0..e2f3b84739685 100644 --- a/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts +++ b/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts @@ -23,16 +23,16 @@ export function envoyproxyMetricsSpecProvider(context: TutorialContext): Tutoria return { id: 'envoyproxyMetrics', name: i18n.translate('home.tutorials.envoyproxyMetrics.nameTitle', { - defaultMessage: 'Envoy Proxy metrics', + defaultMessage: 'Envoy Proxy Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.envoyproxyMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from Envoy Proxy.', + defaultMessage: 'Collect metrics from Envoy Proxy with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.envoyproxyMetrics.longDescription', { defaultMessage: - 'The `envoyproxy` Metricbeat module fetches monitoring metrics from Envoy Proxy. \ + 'The `envoyproxy` Metricbeat module fetches metrics from Envoy Proxy. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-envoyproxy.html', diff --git a/src/plugins/home/server/tutorials/etcd_metrics/index.ts b/src/plugins/home/server/tutorials/etcd_metrics/index.ts index cda16ecf68e34..9ed153c21c257 100644 --- a/src/plugins/home/server/tutorials/etcd_metrics/index.ts +++ b/src/plugins/home/server/tutorials/etcd_metrics/index.ts @@ -23,17 +23,17 @@ export function etcdMetricsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'etcdMetrics', name: i18n.translate('home.tutorials.etcdMetrics.nameTitle', { - defaultMessage: 'Etcd metrics', + defaultMessage: 'Etcd Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.etcdMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from the Etcd server.', + defaultMessage: 'Collect metrics from Etcd servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.etcdMetrics.longDescription', { defaultMessage: - 'The `etcd` Metricbeat module fetches internal metrics from Etcd. \ + 'The `etcd` Metricbeat module fetches metrics from Etcd. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-etcd.html', diff --git a/src/plugins/home/server/tutorials/f5_logs/index.ts b/src/plugins/home/server/tutorials/f5_logs/index.ts index ebcdd4ece7f45..a407d1d3d5142 100644 --- a/src/plugins/home/server/tutorials/f5_logs/index.ts +++ b/src/plugins/home/server/tutorials/f5_logs/index.ts @@ -24,12 +24,12 @@ export function f5LogsSpecProvider(context: TutorialContext): TutorialSchema { return { id: 'f5Logs', name: i18n.translate('home.tutorials.f5Logs.nameTitle', { - defaultMessage: 'F5 logs', + defaultMessage: 'F5 Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.f5Logs.shortDescription', { - defaultMessage: 'Collect F5 Big-IP Access Policy Manager logs over syslog or from a file.', + defaultMessage: 'Collect and parse logs from F5 Big-IP Access Policy Manager with Filebeat.', }), longDescription: i18n.translate('home.tutorials.f5Logs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/fortinet_logs/index.ts b/src/plugins/home/server/tutorials/fortinet_logs/index.ts index 3e7923b680c6e..2f6af3ba47280 100644 --- a/src/plugins/home/server/tutorials/fortinet_logs/index.ts +++ b/src/plugins/home/server/tutorials/fortinet_logs/index.ts @@ -24,12 +24,12 @@ export function fortinetLogsSpecProvider(context: TutorialContext): TutorialSche return { id: 'fortinetLogs', name: i18n.translate('home.tutorials.fortinetLogs.nameTitle', { - defaultMessage: 'Fortinet logs', + defaultMessage: 'Fortinet Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.fortinetLogs.shortDescription', { - defaultMessage: 'Collect Fortinet FortiOS logs over syslog.', + defaultMessage: 'Collect and parse logs from Fortinet FortiOS with Filebeat.', }), longDescription: i18n.translate('home.tutorials.fortinetLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/gcp_logs/index.ts b/src/plugins/home/server/tutorials/gcp_logs/index.ts index feef7d673c5d9..23d8e3364eb69 100644 --- a/src/plugins/home/server/tutorials/gcp_logs/index.ts +++ b/src/plugins/home/server/tutorials/gcp_logs/index.ts @@ -24,12 +24,12 @@ export function gcpLogsSpecProvider(context: TutorialContext): TutorialSchema { return { id: 'gcpLogs', name: i18n.translate('home.tutorials.gcpLogs.nameTitle', { - defaultMessage: 'Google Cloud logs', + defaultMessage: 'Google Cloud Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.gcpLogs.shortDescription', { - defaultMessage: 'Collect Google Cloud audit, firewall, and VPC flow logs.', + defaultMessage: 'Collect and parse logs from Google Cloud Platform with Filebeat.', }), longDescription: i18n.translate('home.tutorials.gcpLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/gcp_metrics/index.ts b/src/plugins/home/server/tutorials/gcp_metrics/index.ts index 5f198ed5f3cf2..7f397c1e1be7b 100644 --- a/src/plugins/home/server/tutorials/gcp_metrics/index.ts +++ b/src/plugins/home/server/tutorials/gcp_metrics/index.ts @@ -23,17 +23,16 @@ export function gcpMetricsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'gcpMetrics', name: i18n.translate('home.tutorials.gcpMetrics.nameTitle', { - defaultMessage: 'Google Cloud metrics', + defaultMessage: 'Google Cloud Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.gcpMetrics.shortDescription', { - defaultMessage: - 'Fetch monitoring metrics from Google Cloud Platform using Stackdriver Monitoring API.', + defaultMessage: 'Collect metrics from Google Cloud Platform with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.gcpMetrics.longDescription', { defaultMessage: - 'The `gcp` Metricbeat module fetches monitoring metrics from Google Cloud Platform using Stackdriver Monitoring API. \ + 'The `gcp` Metricbeat module fetches metrics from Google Cloud Platform using Stackdriver Monitoring API. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-gcp.html', diff --git a/src/plugins/home/server/tutorials/golang_metrics/index.ts b/src/plugins/home/server/tutorials/golang_metrics/index.ts index 85937e0dda0e0..50d09e42e8791 100644 --- a/src/plugins/home/server/tutorials/golang_metrics/index.ts +++ b/src/plugins/home/server/tutorials/golang_metrics/index.ts @@ -23,17 +23,17 @@ export function golangMetricsSpecProvider(context: TutorialContext): TutorialSch return { id: moduleName + 'Metrics', name: i18n.translate('home.tutorials.golangMetrics.nameTitle', { - defaultMessage: 'Golang metrics', + defaultMessage: 'Golang Metrics', }), moduleName, isBeta: true, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.golangMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from a Golang app.', + defaultMessage: 'Collect metrics from Golang applications with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.golangMetrics.longDescription', { defaultMessage: - 'The `{moduleName}` Metricbeat module fetches internal metrics from a Golang app. \ + 'The `{moduleName}` Metricbeat module fetches metrics from a Golang app. \ [Learn more]({learnMoreLink}).', values: { moduleName, diff --git a/src/plugins/home/server/tutorials/gsuite_logs/index.ts b/src/plugins/home/server/tutorials/gsuite_logs/index.ts index 4d23c6b1cfdce..718558321cf78 100644 --- a/src/plugins/home/server/tutorials/gsuite_logs/index.ts +++ b/src/plugins/home/server/tutorials/gsuite_logs/index.ts @@ -24,16 +24,16 @@ export function gsuiteLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'gsuiteLogs', name: i18n.translate('home.tutorials.gsuiteLogs.nameTitle', { - defaultMessage: 'GSuite logs', + defaultMessage: 'GSuite Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.gsuiteLogs.shortDescription', { - defaultMessage: 'Collect GSuite activity reports.', + defaultMessage: 'Collect and parse activity reports from GSuite with Filebeat.', }), longDescription: i18n.translate('home.tutorials.gsuiteLogs.longDescription', { defaultMessage: - 'This is a module for ingesting data from the different GSuite audit reports APIs. \ + 'This is a module for ingesting data from different GSuite audit reports APIs. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-gsuite.html', diff --git a/src/plugins/home/server/tutorials/haproxy_logs/index.ts b/src/plugins/home/server/tutorials/haproxy_logs/index.ts index 0b0fd35f07058..c3765317ecbe0 100644 --- a/src/plugins/home/server/tutorials/haproxy_logs/index.ts +++ b/src/plugins/home/server/tutorials/haproxy_logs/index.ts @@ -24,12 +24,12 @@ export function haproxyLogsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'haproxyLogs', name: i18n.translate('home.tutorials.haproxyLogs.nameTitle', { - defaultMessage: 'HAProxy logs', + defaultMessage: 'HAProxy Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.haproxyLogs.shortDescription', { - defaultMessage: 'Collect HAProxy logs.', + defaultMessage: 'Collect and parse logs from HAProxy servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.haproxyLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/haproxy_metrics/index.ts b/src/plugins/home/server/tutorials/haproxy_metrics/index.ts index e37f0ffc4b916..49f1d32dc4c82 100644 --- a/src/plugins/home/server/tutorials/haproxy_metrics/index.ts +++ b/src/plugins/home/server/tutorials/haproxy_metrics/index.ts @@ -23,17 +23,17 @@ export function haproxyMetricsSpecProvider(context: TutorialContext): TutorialSc return { id: 'haproxyMetrics', name: i18n.translate('home.tutorials.haproxyMetrics.nameTitle', { - defaultMessage: 'HAProxy metrics', + defaultMessage: 'HAProxy Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.haproxyMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from the HAProxy server.', + defaultMessage: 'Collect metrics from HAProxy servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.haproxyMetrics.longDescription', { defaultMessage: - 'The `haproxy` Metricbeat module fetches internal metrics from HAProxy. \ + 'The `haproxy` Metricbeat module fetches metrics from HAProxy. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-haproxy.html', diff --git a/src/plugins/home/server/tutorials/ibmmq_logs/index.ts b/src/plugins/home/server/tutorials/ibmmq_logs/index.ts index 646747d1a49f8..21b60a9ab5a5c 100644 --- a/src/plugins/home/server/tutorials/ibmmq_logs/index.ts +++ b/src/plugins/home/server/tutorials/ibmmq_logs/index.ts @@ -24,12 +24,12 @@ export function ibmmqLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'ibmmqLogs', name: i18n.translate('home.tutorials.ibmmqLogs.nameTitle', { - defaultMessage: 'IBM MQ logs', + defaultMessage: 'IBM MQ Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.ibmmqLogs.shortDescription', { - defaultMessage: 'Collect IBM MQ logs with Filebeat.', + defaultMessage: 'Collect and parse logs from IBM MQ with Filebeat.', }), longDescription: i18n.translate('home.tutorials.ibmmqLogs.longDescription', { defaultMessage: 'Collect IBM MQ logs with Filebeat. \ diff --git a/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts b/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts index 3862bd9ca85eb..706003f0eab48 100644 --- a/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts +++ b/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts @@ -23,16 +23,16 @@ export function ibmmqMetricsSpecProvider(context: TutorialContext): TutorialSche return { id: 'ibmmqMetrics', name: i18n.translate('home.tutorials.ibmmqMetrics.nameTitle', { - defaultMessage: 'IBM MQ metrics', + defaultMessage: 'IBM MQ Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.ibmmqMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from IBM MQ instances.', + defaultMessage: 'Collect metrics from IBM MQ instances with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.ibmmqMetrics.longDescription', { defaultMessage: - 'The `ibmmq` Metricbeat module fetches monitoring metrics from IBM MQ instances \ + 'The `ibmmq` Metricbeat module fetches metrics from IBM MQ instances \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-ibmmq.html', diff --git a/src/plugins/home/server/tutorials/icinga_logs/index.ts b/src/plugins/home/server/tutorials/icinga_logs/index.ts index 0dae93b70343b..dc730022262c2 100644 --- a/src/plugins/home/server/tutorials/icinga_logs/index.ts +++ b/src/plugins/home/server/tutorials/icinga_logs/index.ts @@ -24,12 +24,12 @@ export function icingaLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'icingaLogs', name: i18n.translate('home.tutorials.icingaLogs.nameTitle', { - defaultMessage: 'Icinga logs', + defaultMessage: 'Icinga Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.icingaLogs.shortDescription', { - defaultMessage: 'Collect Icinga main, debug, and startup logs.', + defaultMessage: 'Collect and parse main, debug, and startup logs from Icinga with Filebeat.', }), longDescription: i18n.translate('home.tutorials.icingaLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/iis_logs/index.ts b/src/plugins/home/server/tutorials/iis_logs/index.ts index 5393edf6ab148..0dbc5bbdc75b8 100644 --- a/src/plugins/home/server/tutorials/iis_logs/index.ts +++ b/src/plugins/home/server/tutorials/iis_logs/index.ts @@ -24,12 +24,13 @@ export function iisLogsSpecProvider(context: TutorialContext): TutorialSchema { return { id: 'iisLogs', name: i18n.translate('home.tutorials.iisLogs.nameTitle', { - defaultMessage: 'IIS logs', + defaultMessage: 'IIS Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.iisLogs.shortDescription', { - defaultMessage: 'Collect and parse access and error logs created by the IIS HTTP server.', + defaultMessage: + 'Collect and parse access and error logs from IIS HTTP servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.iisLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/iis_metrics/index.ts b/src/plugins/home/server/tutorials/iis_metrics/index.ts index dbfa474dc9c89..d57e4688ba753 100644 --- a/src/plugins/home/server/tutorials/iis_metrics/index.ts +++ b/src/plugins/home/server/tutorials/iis_metrics/index.ts @@ -28,7 +28,7 @@ export function iisMetricsSpecProvider(context: TutorialContext): TutorialSchema moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.iisMetrics.shortDescription', { - defaultMessage: 'Collect IIS server related metrics.', + defaultMessage: 'Collect metrics from IIS HTTP servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.iisMetrics.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/imperva_logs/index.ts b/src/plugins/home/server/tutorials/imperva_logs/index.ts index 71c3af3809e2e..1cbe707f813ee 100644 --- a/src/plugins/home/server/tutorials/imperva_logs/index.ts +++ b/src/plugins/home/server/tutorials/imperva_logs/index.ts @@ -24,12 +24,12 @@ export function impervaLogsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'impervaLogs', name: i18n.translate('home.tutorials.impervaLogs.nameTitle', { - defaultMessage: 'Imperva logs', + defaultMessage: 'Imperva Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.impervaLogs.shortDescription', { - defaultMessage: 'Collect Imperva SecureSphere logs over syslog or from a file.', + defaultMessage: 'Collect and parse logs from Imperva SecureSphere with Filebeat.', }), longDescription: i18n.translate('home.tutorials.impervaLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/infoblox_logs/index.ts b/src/plugins/home/server/tutorials/infoblox_logs/index.ts index 5329444dfa85f..8dce2bf00b2e2 100644 --- a/src/plugins/home/server/tutorials/infoblox_logs/index.ts +++ b/src/plugins/home/server/tutorials/infoblox_logs/index.ts @@ -24,12 +24,12 @@ export function infobloxLogsSpecProvider(context: TutorialContext): TutorialSche return { id: 'infobloxLogs', name: i18n.translate('home.tutorials.infobloxLogs.nameTitle', { - defaultMessage: 'Infoblox logs', + defaultMessage: 'Infoblox Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.infobloxLogs.shortDescription', { - defaultMessage: 'Collect Infoblox NIOS logs over syslog or from a file.', + defaultMessage: 'Collect and parse logs from Infoblox NIOS with Filebeat.', }), longDescription: i18n.translate('home.tutorials.infobloxLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/iptables_logs/index.ts b/src/plugins/home/server/tutorials/iptables_logs/index.ts index 85faf169f8714..6d298e88a2dfb 100644 --- a/src/plugins/home/server/tutorials/iptables_logs/index.ts +++ b/src/plugins/home/server/tutorials/iptables_logs/index.ts @@ -24,12 +24,12 @@ export function iptablesLogsSpecProvider(context: TutorialContext): TutorialSche return { id: 'iptablesLogs', name: i18n.translate('home.tutorials.iptablesLogs.nameTitle', { - defaultMessage: 'Iptables logs', + defaultMessage: 'Iptables Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.iptablesLogs.shortDescription', { - defaultMessage: 'Collect iptables and ip6tables logs.', + defaultMessage: 'Collect and parse logs from iptables and ip6tables with Filebeat.', }), longDescription: i18n.translate('home.tutorials.iptablesLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/juniper_logs/index.ts b/src/plugins/home/server/tutorials/juniper_logs/index.ts index f9174d8a089e0..7430e4705a5f4 100644 --- a/src/plugins/home/server/tutorials/juniper_logs/index.ts +++ b/src/plugins/home/server/tutorials/juniper_logs/index.ts @@ -29,7 +29,7 @@ export function juniperLogsSpecProvider(context: TutorialContext): TutorialSchem moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.juniperLogs.shortDescription', { - defaultMessage: 'Collect Juniper JUNOS logs over syslog or from a file.', + defaultMessage: 'Collect and parse logs from Juniper JUNOS with Filebeat.', }), longDescription: i18n.translate('home.tutorials.juniperLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/kafka_logs/index.ts b/src/plugins/home/server/tutorials/kafka_logs/index.ts index 5b877cadcbec6..9ccc06eb222c7 100644 --- a/src/plugins/home/server/tutorials/kafka_logs/index.ts +++ b/src/plugins/home/server/tutorials/kafka_logs/index.ts @@ -24,12 +24,12 @@ export function kafkaLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'kafkaLogs', name: i18n.translate('home.tutorials.kafkaLogs.nameTitle', { - defaultMessage: 'Kafka logs', + defaultMessage: 'Kafka Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.kafkaLogs.shortDescription', { - defaultMessage: 'Collect and parse logs created by Kafka.', + defaultMessage: 'Collect and parse logs from Kafka servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.kafkaLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/kafka_metrics/index.ts b/src/plugins/home/server/tutorials/kafka_metrics/index.ts index 92f6744b91cbe..973ec06b58fdf 100644 --- a/src/plugins/home/server/tutorials/kafka_metrics/index.ts +++ b/src/plugins/home/server/tutorials/kafka_metrics/index.ts @@ -23,17 +23,17 @@ export function kafkaMetricsSpecProvider(context: TutorialContext): TutorialSche return { id: 'kafkaMetrics', name: i18n.translate('home.tutorials.kafkaMetrics.nameTitle', { - defaultMessage: 'Kafka metrics', + defaultMessage: 'Kafka Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.kafkaMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from the Kafka server.', + defaultMessage: 'Collect metrics from Kafka servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.kafkaMetrics.longDescription', { defaultMessage: - 'The `kafka` Metricbeat module fetches internal metrics from Kafka. \ + 'The `kafka` Metricbeat module fetches metrics from Kafka. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-kafka.html', diff --git a/src/plugins/home/server/tutorials/kibana_logs/index.ts b/src/plugins/home/server/tutorials/kibana_logs/index.ts index 988af821ef9e3..9863a53700a55 100644 --- a/src/plugins/home/server/tutorials/kibana_logs/index.ts +++ b/src/plugins/home/server/tutorials/kibana_logs/index.ts @@ -29,7 +29,7 @@ export function kibanaLogsSpecProvider(context: TutorialContext): TutorialSchema moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.kibanaLogs.shortDescription', { - defaultMessage: 'Collect Kibana logs.', + defaultMessage: 'Collect and parse logs from Kibana with Filebeat.', }), longDescription: i18n.translate('home.tutorials.kibanaLogs.longDescription', { defaultMessage: 'This is the Kibana module. \ diff --git a/src/plugins/home/server/tutorials/kibana_metrics/index.ts b/src/plugins/home/server/tutorials/kibana_metrics/index.ts index dfe4efe4f7337..3d0eb691ede51 100644 --- a/src/plugins/home/server/tutorials/kibana_metrics/index.ts +++ b/src/plugins/home/server/tutorials/kibana_metrics/index.ts @@ -23,17 +23,17 @@ export function kibanaMetricsSpecProvider(context: TutorialContext): TutorialSch return { id: 'kibanaMetrics', name: i18n.translate('home.tutorials.kibanaMetrics.nameTitle', { - defaultMessage: 'Kibana metrics', + defaultMessage: 'Kibana Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.kibanaMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from Kibana.', + defaultMessage: 'Collect metrics from Kibana with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.kibanaMetrics.longDescription', { defaultMessage: - 'The `kibana` Metricbeat module fetches internal metrics from Kibana. \ + 'The `kibana` Metricbeat module fetches metrics from Kibana. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-kibana.html', diff --git a/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts b/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts index 4a694560f5c28..9c66125ee0cfe 100644 --- a/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts +++ b/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts @@ -23,16 +23,16 @@ export function kubernetesMetricsSpecProvider(context: TutorialContext): Tutoria return { id: 'kubernetesMetrics', name: i18n.translate('home.tutorials.kubernetesMetrics.nameTitle', { - defaultMessage: 'Kubernetes metrics', + defaultMessage: 'Kubernetes Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.kubernetesMetrics.shortDescription', { - defaultMessage: 'Fetch metrics from your Kubernetes installation.', + defaultMessage: 'Collect metrics from Kubernetes installations with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.kubernetesMetrics.longDescription', { defaultMessage: - 'The `kubernetes` Metricbeat module fetches metrics from the Kubernetes APIs. \ + 'The `kubernetes` Metricbeat module fetches metrics from Kubernetes APIs. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-kubernetes.html', diff --git a/src/plugins/home/server/tutorials/logstash_logs/index.ts b/src/plugins/home/server/tutorials/logstash_logs/index.ts index 55491d45df28c..688ad8245b78d 100644 --- a/src/plugins/home/server/tutorials/logstash_logs/index.ts +++ b/src/plugins/home/server/tutorials/logstash_logs/index.ts @@ -24,12 +24,12 @@ export function logstashLogsSpecProvider(context: TutorialContext): TutorialSche return { id: 'logstashLogs', name: i18n.translate('home.tutorials.logstashLogs.nameTitle', { - defaultMessage: 'Logstash logs', + defaultMessage: 'Logstash Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.logstashLogs.shortDescription', { - defaultMessage: 'Collect Logstash main and slow logs.', + defaultMessage: 'Collect and parse main and slow logs from Logstash with Filebeat.', }), longDescription: i18n.translate('home.tutorials.logstashLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/logstash_metrics/index.ts b/src/plugins/home/server/tutorials/logstash_metrics/index.ts index e7d3fae011bd2..9ae4bcdcecbf1 100644 --- a/src/plugins/home/server/tutorials/logstash_metrics/index.ts +++ b/src/plugins/home/server/tutorials/logstash_metrics/index.ts @@ -23,17 +23,17 @@ export function logstashMetricsSpecProvider(context: TutorialContext): TutorialS return { id: moduleName + 'Metrics', name: i18n.translate('home.tutorials.logstashMetrics.nameTitle', { - defaultMessage: 'Logstash metrics', + defaultMessage: 'Logstash Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.logstashMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from a Logstash server.', + defaultMessage: 'Collect metrics from Logstash servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.logstashMetrics.longDescription', { defaultMessage: - 'The `{moduleName}` Metricbeat module fetches internal metrics from a Logstash server. \ + 'The `{moduleName}` Metricbeat module fetches metrics from a Logstash server. \ [Learn more]({learnMoreLink}).', values: { moduleName, diff --git a/src/plugins/home/server/tutorials/memcached_metrics/index.ts b/src/plugins/home/server/tutorials/memcached_metrics/index.ts index 15df179b44a9e..891567f72ca7c 100644 --- a/src/plugins/home/server/tutorials/memcached_metrics/index.ts +++ b/src/plugins/home/server/tutorials/memcached_metrics/index.ts @@ -23,17 +23,17 @@ export function memcachedMetricsSpecProvider(context: TutorialContext): Tutorial return { id: 'memcachedMetrics', name: i18n.translate('home.tutorials.memcachedMetrics.nameTitle', { - defaultMessage: 'Memcached metrics', + defaultMessage: 'Memcached Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.memcachedMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from the Memcached server.', + defaultMessage: 'Collect metrics from Memcached servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.memcachedMetrics.longDescription', { defaultMessage: - 'The `memcached` Metricbeat module fetches internal metrics from Memcached. \ + 'The `memcached` Metricbeat module fetches metrics from Memcached. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-memcached.html', diff --git a/src/plugins/home/server/tutorials/microsoft_logs/index.ts b/src/plugins/home/server/tutorials/microsoft_logs/index.ts index 52401df1f9eb7..88893e22bc9ff 100644 --- a/src/plugins/home/server/tutorials/microsoft_logs/index.ts +++ b/src/plugins/home/server/tutorials/microsoft_logs/index.ts @@ -24,12 +24,12 @@ export function microsoftLogsSpecProvider(context: TutorialContext): TutorialSch return { id: 'microsoftLogs', name: i18n.translate('home.tutorials.microsoftLogs.nameTitle', { - defaultMessage: 'Microsoft Defender ATP logs', + defaultMessage: 'Microsoft Defender ATP Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.microsoftLogs.shortDescription', { - defaultMessage: 'Collect Microsoft Defender ATP alerts.', + defaultMessage: 'Collect and parse alerts from Microsoft Defender ATP with Filebeat.', }), longDescription: i18n.translate('home.tutorials.microsoftLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/misp_logs/index.ts b/src/plugins/home/server/tutorials/misp_logs/index.ts index b7611b543bab1..ea2147a296534 100644 --- a/src/plugins/home/server/tutorials/misp_logs/index.ts +++ b/src/plugins/home/server/tutorials/misp_logs/index.ts @@ -24,12 +24,12 @@ export function mispLogsSpecProvider(context: TutorialContext): TutorialSchema { return { id: 'mispLogs', name: i18n.translate('home.tutorials.mispLogs.nameTitle', { - defaultMessage: 'MISP threat intel logs', + defaultMessage: 'MISP threat intel Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.mispLogs.shortDescription', { - defaultMessage: 'Collect MISP threat intelligence data with Filebeat.', + defaultMessage: 'Collect and parse logs from MISP threat intelligence with Filebeat.', }), longDescription: i18n.translate('home.tutorials.mispLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/mongodb_logs/index.ts b/src/plugins/home/server/tutorials/mongodb_logs/index.ts index 3c189c04da43b..a7f9869d440ed 100644 --- a/src/plugins/home/server/tutorials/mongodb_logs/index.ts +++ b/src/plugins/home/server/tutorials/mongodb_logs/index.ts @@ -24,12 +24,12 @@ export function mongodbLogsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'mongodbLogs', name: i18n.translate('home.tutorials.mongodbLogs.nameTitle', { - defaultMessage: 'MongoDB logs', + defaultMessage: 'MongoDB Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.mongodbLogs.shortDescription', { - defaultMessage: 'Collect MongoDB logs.', + defaultMessage: 'Collect and parse logs from MongoDB servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.mongodbLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/mongodb_metrics/index.ts b/src/plugins/home/server/tutorials/mongodb_metrics/index.ts index 121310fba6f3a..cc0ecc0574fa9 100644 --- a/src/plugins/home/server/tutorials/mongodb_metrics/index.ts +++ b/src/plugins/home/server/tutorials/mongodb_metrics/index.ts @@ -23,16 +23,16 @@ export function mongodbMetricsSpecProvider(context: TutorialContext): TutorialSc return { id: 'mongodbMetrics', name: i18n.translate('home.tutorials.mongodbMetrics.nameTitle', { - defaultMessage: 'MongoDB metrics', + defaultMessage: 'MongoDB Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.mongodbMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from MongoDB.', + defaultMessage: 'Collect metrics from MongoDB servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.mongodbMetrics.longDescription', { defaultMessage: - 'The `mongodb` Metricbeat module fetches internal metrics from the MongoDB server. \ + 'The `mongodb` Metricbeat module fetches metrics from MongoDB server. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-mongodb.html', diff --git a/src/plugins/home/server/tutorials/mssql_logs/index.ts b/src/plugins/home/server/tutorials/mssql_logs/index.ts index 567080910b7fe..06cafd95283c8 100644 --- a/src/plugins/home/server/tutorials/mssql_logs/index.ts +++ b/src/plugins/home/server/tutorials/mssql_logs/index.ts @@ -24,12 +24,12 @@ export function mssqlLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'mssqlLogs', name: i18n.translate('home.tutorials.mssqlLogs.nameTitle', { - defaultMessage: 'MSSQL logs', + defaultMessage: 'Microsoft SQL Server Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.mssqlLogs.shortDescription', { - defaultMessage: 'Collect MSSQL logs.', + defaultMessage: 'Collect and parse logs from Microsoft SQL Server instances with Filebeat.', }), longDescription: i18n.translate('home.tutorials.mssqlLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/mssql_metrics/index.ts b/src/plugins/home/server/tutorials/mssql_metrics/index.ts index 998cefe2de004..e3c9e3c338209 100644 --- a/src/plugins/home/server/tutorials/mssql_metrics/index.ts +++ b/src/plugins/home/server/tutorials/mssql_metrics/index.ts @@ -28,7 +28,7 @@ export function mssqlMetricsSpecProvider(context: TutorialContext): TutorialSche moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.mssqlMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from a Microsoft SQL Server instance', + defaultMessage: 'Collect metrics from Microsoft SQL Server instances with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.mssqlMetrics.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/munin_metrics/index.ts b/src/plugins/home/server/tutorials/munin_metrics/index.ts index 1abd321e4c738..12621d05d0766 100644 --- a/src/plugins/home/server/tutorials/munin_metrics/index.ts +++ b/src/plugins/home/server/tutorials/munin_metrics/index.ts @@ -23,18 +23,18 @@ export function muninMetricsSpecProvider(context: TutorialContext): TutorialSche return { id: 'muninMetrics', name: i18n.translate('home.tutorials.muninMetrics.nameTitle', { - defaultMessage: 'Munin metrics', + defaultMessage: 'Munin Metrics', }), moduleName, euiIconType: '/plugins/home/assets/logos/munin.svg', isBeta: true, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.muninMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from the Munin server.', + defaultMessage: 'Collect metrics from Munin servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.muninMetrics.longDescription', { defaultMessage: - 'The `munin` Metricbeat module fetches internal metrics from Munin. \ + 'The `munin` Metricbeat module fetches metrics from Munin. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-munin.html', diff --git a/src/plugins/home/server/tutorials/mysql_logs/index.ts b/src/plugins/home/server/tutorials/mysql_logs/index.ts index a788e736d2964..b0c6f0e69dcfb 100644 --- a/src/plugins/home/server/tutorials/mysql_logs/index.ts +++ b/src/plugins/home/server/tutorials/mysql_logs/index.ts @@ -24,12 +24,12 @@ export function mysqlLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'mysqlLogs', name: i18n.translate('home.tutorials.mysqlLogs.nameTitle', { - defaultMessage: 'MySQL logs', + defaultMessage: 'MySQL Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.mysqlLogs.shortDescription', { - defaultMessage: 'Collect and parse error and slow logs created by MySQL.', + defaultMessage: 'Collect and parse logs from MySQL servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.mysqlLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/mysql_metrics/index.ts b/src/plugins/home/server/tutorials/mysql_metrics/index.ts index 078a96f8110df..09c55dc81ff84 100644 --- a/src/plugins/home/server/tutorials/mysql_metrics/index.ts +++ b/src/plugins/home/server/tutorials/mysql_metrics/index.ts @@ -23,16 +23,16 @@ export function mysqlMetricsSpecProvider(context: TutorialContext): TutorialSche return { id: 'mysqlMetrics', name: i18n.translate('home.tutorials.mysqlMetrics.nameTitle', { - defaultMessage: 'MySQL metrics', + defaultMessage: 'MySQL Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.mysqlMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from MySQL.', + defaultMessage: 'Collect metrics from MySQL servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.mysqlMetrics.longDescription', { defaultMessage: - 'The `mysql` Metricbeat module fetches internal metrics from the MySQL server. \ + 'The `mysql` Metricbeat module fetches metrics from MySQL server. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-mysql.html', diff --git a/src/plugins/home/server/tutorials/nats_logs/index.ts b/src/plugins/home/server/tutorials/nats_logs/index.ts index a1dc24080bc0d..b6ef0a192d92f 100644 --- a/src/plugins/home/server/tutorials/nats_logs/index.ts +++ b/src/plugins/home/server/tutorials/nats_logs/index.ts @@ -24,13 +24,13 @@ export function natsLogsSpecProvider(context: TutorialContext): TutorialSchema { return { id: 'natsLogs', name: i18n.translate('home.tutorials.natsLogs.nameTitle', { - defaultMessage: 'NATS logs', + defaultMessage: 'NATS Logs', }), moduleName, category: TutorialsCategory.LOGGING, isBeta: true, shortDescription: i18n.translate('home.tutorials.natsLogs.shortDescription', { - defaultMessage: 'Collect and parse logs created by Nats.', + defaultMessage: 'Collect and parse logs from NATS servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.natsLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/nats_metrics/index.ts b/src/plugins/home/server/tutorials/nats_metrics/index.ts index 11494e5dc57d0..54f034ad44b19 100644 --- a/src/plugins/home/server/tutorials/nats_metrics/index.ts +++ b/src/plugins/home/server/tutorials/nats_metrics/index.ts @@ -23,16 +23,16 @@ export function natsMetricsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'natsMetrics', name: i18n.translate('home.tutorials.natsMetrics.nameTitle', { - defaultMessage: 'NATS metrics', + defaultMessage: 'NATS Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.natsMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from the Nats server.', + defaultMessage: 'Collect metrics from NATS servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.natsMetrics.longDescription', { defaultMessage: - 'The `nats` Metricbeat module fetches monitoring metrics from Nats. \ + 'The `nats` Metricbeat module fetches metrics from Nats. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-nats.html', diff --git a/src/plugins/home/server/tutorials/netflow_logs/index.ts b/src/plugins/home/server/tutorials/netflow_logs/index.ts index e8404e93ae355..c659d9c1d31b1 100644 --- a/src/plugins/home/server/tutorials/netflow_logs/index.ts +++ b/src/plugins/home/server/tutorials/netflow_logs/index.ts @@ -24,12 +24,12 @@ export function netflowLogsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'netflowLogs', name: i18n.translate('home.tutorials.netflowLogs.nameTitle', { - defaultMessage: 'NetFlow / IPFIX Collector', + defaultMessage: 'NetFlow / IPFIX Records', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.netflowLogs.shortDescription', { - defaultMessage: 'Collect NetFlow and IPFIX flow records.', + defaultMessage: 'Collect records from NetFlow and IPFIX flow with Filebeat.', }), longDescription: i18n.translate('home.tutorials.netflowLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/netscout_logs/index.ts b/src/plugins/home/server/tutorials/netscout_logs/index.ts index 395fbb8b49d39..e6c22947f8057 100644 --- a/src/plugins/home/server/tutorials/netscout_logs/index.ts +++ b/src/plugins/home/server/tutorials/netscout_logs/index.ts @@ -24,12 +24,12 @@ export function netscoutLogsSpecProvider(context: TutorialContext): TutorialSche return { id: 'netscoutLogs', name: i18n.translate('home.tutorials.netscoutLogs.nameTitle', { - defaultMessage: 'Arbor Peakflow logs', + defaultMessage: 'Arbor Peakflow Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.netscoutLogs.shortDescription', { - defaultMessage: 'Collect Netscout Arbor Peakflow SP logs over syslog or from a file.', + defaultMessage: 'Collect and parse logs from Netscout Arbor Peakflow SP with Filebeat.', }), longDescription: i18n.translate('home.tutorials.netscoutLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/nginx_logs/index.ts b/src/plugins/home/server/tutorials/nginx_logs/index.ts index 90ec6737c2461..e6f2fc4efb01c 100644 --- a/src/plugins/home/server/tutorials/nginx_logs/index.ts +++ b/src/plugins/home/server/tutorials/nginx_logs/index.ts @@ -24,12 +24,12 @@ export function nginxLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'nginxLogs', name: i18n.translate('home.tutorials.nginxLogs.nameTitle', { - defaultMessage: 'Nginx logs', + defaultMessage: 'Nginx Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.nginxLogs.shortDescription', { - defaultMessage: 'Collect and parse access and error logs created by the Nginx HTTP server.', + defaultMessage: 'Collect and parse logs from Nginx HTTP servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.nginxLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/nginx_metrics/index.ts b/src/plugins/home/server/tutorials/nginx_metrics/index.ts index 12f67a26dcf29..680dd664912d3 100644 --- a/src/plugins/home/server/tutorials/nginx_metrics/index.ts +++ b/src/plugins/home/server/tutorials/nginx_metrics/index.ts @@ -23,16 +23,16 @@ export function nginxMetricsSpecProvider(context: TutorialContext): TutorialSche return { id: 'nginxMetrics', name: i18n.translate('home.tutorials.nginxMetrics.nameTitle', { - defaultMessage: 'Nginx metrics', + defaultMessage: 'Nginx Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.nginxMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from the Nginx HTTP server.', + defaultMessage: 'Collect metrics from Nginx HTTP servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.nginxMetrics.longDescription', { defaultMessage: - 'The `nginx` Metricbeat module fetches internal metrics from the Nginx HTTP server. \ + 'The `nginx` Metricbeat module fetches metrics from Nginx HTTP server. \ The module scrapes the server status data from the web page generated by the \ {statusModuleLink}, \ which must be enabled in your Nginx installation. \ diff --git a/src/plugins/home/server/tutorials/o365_logs/index.ts b/src/plugins/home/server/tutorials/o365_logs/index.ts index e3663e2c3cd78..3cd4d3a5c5e18 100644 --- a/src/plugins/home/server/tutorials/o365_logs/index.ts +++ b/src/plugins/home/server/tutorials/o365_logs/index.ts @@ -24,12 +24,12 @@ export function o365LogsSpecProvider(context: TutorialContext): TutorialSchema { return { id: 'o365Logs', name: i18n.translate('home.tutorials.o365Logs.nameTitle', { - defaultMessage: 'Office 365 logs', + defaultMessage: 'Office 365 Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.o365Logs.shortDescription', { - defaultMessage: 'Collect Office 365 activity logs via the Office 365 API.', + defaultMessage: 'Collect and parse logs from Office 365 with Filebeat.', }), longDescription: i18n.translate('home.tutorials.o365Logs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/okta_logs/index.ts b/src/plugins/home/server/tutorials/okta_logs/index.ts index 62cde4b5128c3..aad18409de329 100644 --- a/src/plugins/home/server/tutorials/okta_logs/index.ts +++ b/src/plugins/home/server/tutorials/okta_logs/index.ts @@ -24,12 +24,12 @@ export function oktaLogsSpecProvider(context: TutorialContext): TutorialSchema { return { id: 'oktaLogs', name: i18n.translate('home.tutorials.oktaLogs.nameTitle', { - defaultMessage: 'Okta logs', + defaultMessage: 'Okta Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.oktaLogs.shortDescription', { - defaultMessage: 'Collect the Okta system log via the Okta API.', + defaultMessage: 'Collect and parse logs from the Okta API with Filebeat.', }), longDescription: i18n.translate('home.tutorials.oktaLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts b/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts index acbddf5169881..02625b341549b 100644 --- a/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts +++ b/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts @@ -23,12 +23,13 @@ export function openmetricsMetricsSpecProvider(context: TutorialContext): Tutori return { id: 'openmetricsMetrics', name: i18n.translate('home.tutorials.openmetricsMetrics.nameTitle', { - defaultMessage: 'OpenMetrics metrics', + defaultMessage: 'OpenMetrics Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.openmetricsMetrics.shortDescription', { - defaultMessage: 'Fetch metrics from an endpoint that serves metrics in OpenMetrics format.', + defaultMessage: + 'Collect metrics from an endpoint that serves metrics in OpenMetrics format with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.openmetricsMetrics.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/oracle_metrics/index.ts b/src/plugins/home/server/tutorials/oracle_metrics/index.ts index 9b63e82c21ccd..14cf5392c5231 100644 --- a/src/plugins/home/server/tutorials/oracle_metrics/index.ts +++ b/src/plugins/home/server/tutorials/oracle_metrics/index.ts @@ -23,17 +23,17 @@ export function oracleMetricsSpecProvider(context: TutorialContext): TutorialSch return { id: moduleName + 'Metrics', name: i18n.translate('home.tutorials.oracleMetrics.nameTitle', { - defaultMessage: 'oracle metrics', + defaultMessage: 'oracle Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.oracleMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from a Oracle server.', + defaultMessage: 'Collect metrics from Oracle servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.oracleMetrics.longDescription', { defaultMessage: - 'The `{moduleName}` Metricbeat module fetches internal metrics from a Oracle server. \ + 'The `{moduleName}` Metricbeat module fetches metrics from a Oracle server. \ [Learn more]({learnMoreLink}).', values: { moduleName, diff --git a/src/plugins/home/server/tutorials/osquery_logs/index.ts b/src/plugins/home/server/tutorials/osquery_logs/index.ts index 6bacbed57792c..4f87fc4e256e1 100644 --- a/src/plugins/home/server/tutorials/osquery_logs/index.ts +++ b/src/plugins/home/server/tutorials/osquery_logs/index.ts @@ -24,12 +24,12 @@ export function osqueryLogsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'osqueryLogs', name: i18n.translate('home.tutorials.osqueryLogs.nameTitle', { - defaultMessage: 'Osquery logs', + defaultMessage: 'Osquery Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.osqueryLogs.shortDescription', { - defaultMessage: 'Collect osquery logs in JSON format.', + defaultMessage: 'Collect and parse logs from Osquery with Filebeat.', }), longDescription: i18n.translate('home.tutorials.osqueryLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/panw_logs/index.ts b/src/plugins/home/server/tutorials/panw_logs/index.ts index 3ca839556d756..f5158c48f30d5 100644 --- a/src/plugins/home/server/tutorials/panw_logs/index.ts +++ b/src/plugins/home/server/tutorials/panw_logs/index.ts @@ -24,13 +24,13 @@ export function panwLogsSpecProvider(context: TutorialContext): TutorialSchema { return { id: 'panwLogs', name: i18n.translate('home.tutorials.panwLogs.nameTitle', { - defaultMessage: 'Palo Alto Networks PAN-OS logs', + defaultMessage: 'Palo Alto Networks PAN-OS Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.panwLogs.shortDescription', { defaultMessage: - 'Collect Palo Alto Networks PAN-OS threat and traffic logs over syslog or from a log file.', + 'Collect and parse threat and traffic logs from Palo Alto Networks PAN-OS with Filebeat.', }), longDescription: i18n.translate('home.tutorials.panwLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts b/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts index ed67960ab5a1c..40b35984fb17a 100644 --- a/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts +++ b/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts @@ -23,17 +23,17 @@ export function phpfpmMetricsSpecProvider(context: TutorialContext): TutorialSch return { id: 'phpfpmMetrics', name: i18n.translate('home.tutorials.phpFpmMetrics.nameTitle', { - defaultMessage: 'PHP-FPM metrics', + defaultMessage: 'PHP-FPM Metrics', }), moduleName, category: TutorialsCategory.METRICS, isBeta: false, shortDescription: i18n.translate('home.tutorials.phpFpmMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from PHP-FPM.', + defaultMessage: 'Collect metrics from PHP-FPM with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.phpFpmMetrics.longDescription', { defaultMessage: - 'The `php_fpm` Metricbeat module fetches internal metrics from the PHP-FPM server. \ + 'The `php_fpm` Metricbeat module fetches metrics from PHP-FPM server. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-php_fpm.html', diff --git a/src/plugins/home/server/tutorials/postgresql_logs/index.ts b/src/plugins/home/server/tutorials/postgresql_logs/index.ts index c5f5d879ac35d..3a092e61b0bd9 100644 --- a/src/plugins/home/server/tutorials/postgresql_logs/index.ts +++ b/src/plugins/home/server/tutorials/postgresql_logs/index.ts @@ -24,12 +24,12 @@ export function postgresqlLogsSpecProvider(context: TutorialContext): TutorialSc return { id: 'postgresqlLogs', name: i18n.translate('home.tutorials.postgresqlLogs.nameTitle', { - defaultMessage: 'PostgreSQL logs', + defaultMessage: 'PostgreSQL Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.postgresqlLogs.shortDescription', { - defaultMessage: 'Collect and parse error and slow logs created by PostgreSQL.', + defaultMessage: 'Collect and parse logs from PostgreSQL servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.postgresqlLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/postgresql_metrics/index.ts b/src/plugins/home/server/tutorials/postgresql_metrics/index.ts index ca20efb44bca7..501ea252cd16f 100644 --- a/src/plugins/home/server/tutorials/postgresql_metrics/index.ts +++ b/src/plugins/home/server/tutorials/postgresql_metrics/index.ts @@ -23,17 +23,17 @@ export function postgresqlMetricsSpecProvider(context: TutorialContext): Tutoria return { id: 'postgresqlMetrics', name: i18n.translate('home.tutorials.postgresqlMetrics.nameTitle', { - defaultMessage: 'PostgreSQL metrics', + defaultMessage: 'PostgreSQL Metrics', }), moduleName, category: TutorialsCategory.METRICS, isBeta: false, shortDescription: i18n.translate('home.tutorials.postgresqlMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from PostgreSQL.', + defaultMessage: 'Collect metrics from PostgreSQL servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.postgresqlMetrics.longDescription', { defaultMessage: - 'The `postgresql` Metricbeat module fetches internal metrics from the PostgreSQL server. \ + 'The `postgresql` Metricbeat module fetches metrics from PostgreSQL server. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-postgresql.html', diff --git a/src/plugins/home/server/tutorials/prometheus_metrics/index.ts b/src/plugins/home/server/tutorials/prometheus_metrics/index.ts index ee05770d65108..2f422e5e3be70 100644 --- a/src/plugins/home/server/tutorials/prometheus_metrics/index.ts +++ b/src/plugins/home/server/tutorials/prometheus_metrics/index.ts @@ -23,13 +23,13 @@ export function prometheusMetricsSpecProvider(context: TutorialContext): Tutoria return { id: moduleName + 'Metrics', name: i18n.translate('home.tutorials.prometheusMetrics.nameTitle', { - defaultMessage: 'Prometheus metrics', + defaultMessage: 'Prometheus Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.prometheusMetrics.shortDescription', { - defaultMessage: 'Fetch metrics from a Prometheus exporter.', + defaultMessage: 'Collect metrics from Prometheus exporters with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.prometheusMetrics.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/rabbitmq_logs/index.ts b/src/plugins/home/server/tutorials/rabbitmq_logs/index.ts index 0fbdb48236832..8a1634e7da038 100644 --- a/src/plugins/home/server/tutorials/rabbitmq_logs/index.ts +++ b/src/plugins/home/server/tutorials/rabbitmq_logs/index.ts @@ -24,12 +24,12 @@ export function rabbitmqLogsSpecProvider(context: TutorialContext): TutorialSche return { id: 'rabbitmqLogs', name: i18n.translate('home.tutorials.rabbitmqLogs.nameTitle', { - defaultMessage: 'RabbitMQ logs', + defaultMessage: 'RabbitMQ Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.rabbitmqLogs.shortDescription', { - defaultMessage: 'Collect RabbitMQ logs.', + defaultMessage: 'Collect and parse logs from RabbitMQ servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.rabbitmqLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts b/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts index b58f936f205b2..abfc895088d91 100644 --- a/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts +++ b/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts @@ -23,16 +23,16 @@ export function rabbitmqMetricsSpecProvider(context: TutorialContext): TutorialS return { id: 'rabbitmqMetrics', name: i18n.translate('home.tutorials.rabbitmqMetrics.nameTitle', { - defaultMessage: 'RabbitMQ metrics', + defaultMessage: 'RabbitMQ Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.rabbitmqMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from the RabbitMQ server.', + defaultMessage: 'Collect metrics from RabbitMQ servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.rabbitmqMetrics.longDescription', { defaultMessage: - 'The `rabbitmq` Metricbeat module fetches internal metrics from the RabbitMQ server. \ + 'The `rabbitmq` Metricbeat module fetches metrics from RabbitMQ server. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-rabbitmq.html', diff --git a/src/plugins/home/server/tutorials/radware_logs/index.ts b/src/plugins/home/server/tutorials/radware_logs/index.ts index 28392cf9c4362..3e918a0a4064c 100644 --- a/src/plugins/home/server/tutorials/radware_logs/index.ts +++ b/src/plugins/home/server/tutorials/radware_logs/index.ts @@ -24,12 +24,12 @@ export function radwareLogsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'radwareLogs', name: i18n.translate('home.tutorials.radwareLogs.nameTitle', { - defaultMessage: 'Radware DefensePro logs', + defaultMessage: 'Radware DefensePro Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.radwareLogs.shortDescription', { - defaultMessage: 'Collect Radware DefensePro logs over syslog or from a file.', + defaultMessage: 'Collect and parse logs from Radware DefensePro with Filebeat.', }), longDescription: i18n.translate('home.tutorials.radwareLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/redis_logs/index.ts b/src/plugins/home/server/tutorials/redis_logs/index.ts index 0f3a5aa812f49..f6aada27dec48 100644 --- a/src/plugins/home/server/tutorials/redis_logs/index.ts +++ b/src/plugins/home/server/tutorials/redis_logs/index.ts @@ -24,12 +24,12 @@ export function redisLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'redisLogs', name: i18n.translate('home.tutorials.redisLogs.nameTitle', { - defaultMessage: 'Redis logs', + defaultMessage: 'Redis Logs', }), moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.redisLogs.shortDescription', { - defaultMessage: 'Collect and parse error and slow logs created by Redis.', + defaultMessage: 'Collect and parse logs from Redis servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.redisLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/redis_metrics/index.ts b/src/plugins/home/server/tutorials/redis_metrics/index.ts index 1b4ee7290a6d0..2bb300c48ff65 100644 --- a/src/plugins/home/server/tutorials/redis_metrics/index.ts +++ b/src/plugins/home/server/tutorials/redis_metrics/index.ts @@ -23,16 +23,16 @@ export function redisMetricsSpecProvider(context: TutorialContext): TutorialSche return { id: 'redisMetrics', name: i18n.translate('home.tutorials.redisMetrics.nameTitle', { - defaultMessage: 'Redis metrics', + defaultMessage: 'Redis Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.redisMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from Redis.', + defaultMessage: 'Collect metrics from Redis servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.redisMetrics.longDescription', { defaultMessage: - 'The `redis` Metricbeat module fetches internal metrics from the Redis server. \ + 'The `redis` Metricbeat module fetches metrics from Redis server. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-redis.html', diff --git a/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts b/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts index be8de9c3eab4d..62e1386f29dbb 100644 --- a/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts +++ b/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts @@ -23,16 +23,16 @@ export function redisenterpriseMetricsSpecProvider(context: TutorialContext): Tu return { id: 'redisenterpriseMetrics', name: i18n.translate('home.tutorials.redisenterpriseMetrics.nameTitle', { - defaultMessage: 'Redis Enterprise metrics', + defaultMessage: 'Redis Enterprise Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.redisenterpriseMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from Redis Enterprise Server.', + defaultMessage: 'Collect metrics from Redis Enterprise servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.redisenterpriseMetrics.longDescription', { defaultMessage: - 'The `redisenterprise` Metricbeat module fetches monitoring metrics from Redis Enterprise Server \ + 'The `redisenterprise` Metricbeat module fetches metrics from Redis Enterprise Server \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-redisenterprise.html', diff --git a/src/plugins/home/server/tutorials/santa_logs/index.ts b/src/plugins/home/server/tutorials/santa_logs/index.ts index 10d1506438b62..da9f2e940066e 100644 --- a/src/plugins/home/server/tutorials/santa_logs/index.ts +++ b/src/plugins/home/server/tutorials/santa_logs/index.ts @@ -24,12 +24,12 @@ export function santaLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'santaLogs', name: i18n.translate('home.tutorials.santaLogs.nameTitle', { - defaultMessage: 'Google Santa logs', + defaultMessage: 'Google Santa Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.santaLogs.shortDescription', { - defaultMessage: 'Collect Google Santa logs about process executions on MacOS.', + defaultMessage: 'Collect and parse logs from Google Santa systems with Filebeat.', }), longDescription: i18n.translate('home.tutorials.santaLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/sonicwall_logs/index.ts b/src/plugins/home/server/tutorials/sonicwall_logs/index.ts index 1fa711327a07d..04bf7a3968320 100644 --- a/src/plugins/home/server/tutorials/sonicwall_logs/index.ts +++ b/src/plugins/home/server/tutorials/sonicwall_logs/index.ts @@ -24,12 +24,12 @@ export function sonicwallLogsSpecProvider(context: TutorialContext): TutorialSch return { id: 'sonicwallLogs', name: i18n.translate('home.tutorials.sonicwallLogs.nameTitle', { - defaultMessage: 'Sonicwall FW logs', + defaultMessage: 'Sonicwall FW Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.sonicwallLogs.shortDescription', { - defaultMessage: 'Collect Sonicwall-FW logs over syslog or from a file.', + defaultMessage: 'Collect and parse logs from Sonicwall-FW with Filebeat.', }), longDescription: i18n.translate('home.tutorials.sonicwallLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/sophos_logs/index.ts b/src/plugins/home/server/tutorials/sophos_logs/index.ts index 35b27973a55ec..4fadcecb6e1bd 100644 --- a/src/plugins/home/server/tutorials/sophos_logs/index.ts +++ b/src/plugins/home/server/tutorials/sophos_logs/index.ts @@ -24,12 +24,12 @@ export function sophosLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'sophosLogs', name: i18n.translate('home.tutorials.sophosLogs.nameTitle', { - defaultMessage: 'Sophos logs', + defaultMessage: 'Sophos Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.sophosLogs.shortDescription', { - defaultMessage: 'Collect Sophos XG SFOS logs over syslog.', + defaultMessage: 'Collect and parse logs from Sophos XG SFOS with Filebeat.', }), longDescription: i18n.translate('home.tutorials.sophosLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/squid_logs/index.ts b/src/plugins/home/server/tutorials/squid_logs/index.ts index d8d0bb6c0829b..2d8f055d7fa6b 100644 --- a/src/plugins/home/server/tutorials/squid_logs/index.ts +++ b/src/plugins/home/server/tutorials/squid_logs/index.ts @@ -24,12 +24,12 @@ export function squidLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'squidLogs', name: i18n.translate('home.tutorials.squidLogs.nameTitle', { - defaultMessage: 'Squid logs', + defaultMessage: 'Squid Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.squidLogs.shortDescription', { - defaultMessage: 'Collect Squid logs over syslog or from a file.', + defaultMessage: 'Collect and parse logs from Squid servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.squidLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/stan_metrics/index.ts b/src/plugins/home/server/tutorials/stan_metrics/index.ts index ceb6084b539e6..0b3c0352b663d 100644 --- a/src/plugins/home/server/tutorials/stan_metrics/index.ts +++ b/src/plugins/home/server/tutorials/stan_metrics/index.ts @@ -23,16 +23,16 @@ export function stanMetricsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'stanMetrics', name: i18n.translate('home.tutorials.stanMetrics.nameTitle', { - defaultMessage: 'STAN metrics', + defaultMessage: 'STAN Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.stanMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from the STAN server.', + defaultMessage: 'Collect metrics from STAN servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.stanMetrics.longDescription', { defaultMessage: - 'The `stan` Metricbeat module fetches monitoring metrics from STAN. \ + 'The `stan` Metricbeat module fetches metrics from STAN. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-stan.html', diff --git a/src/plugins/home/server/tutorials/statsd_metrics/index.ts b/src/plugins/home/server/tutorials/statsd_metrics/index.ts index 472c1406db386..1be010a01d5a6 100644 --- a/src/plugins/home/server/tutorials/statsd_metrics/index.ts +++ b/src/plugins/home/server/tutorials/statsd_metrics/index.ts @@ -20,16 +20,16 @@ export function statsdMetricsSpecProvider(context: TutorialContext): TutorialSch return { id: 'statsdMetrics', name: i18n.translate('home.tutorials.statsdMetrics.nameTitle', { - defaultMessage: 'Statsd metrics', + defaultMessage: 'Statsd Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.statsdMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from statsd.', + defaultMessage: 'Collect metrics from Statsd servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.statsdMetrics.longDescription', { defaultMessage: - 'The `statsd` Metricbeat module fetches monitoring metrics from statsd. \ + 'The `statsd` Metricbeat module fetches metrics from statsd. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-statsd.html', diff --git a/src/plugins/home/server/tutorials/suricata_logs/index.ts b/src/plugins/home/server/tutorials/suricata_logs/index.ts index 3bb2b93b6301a..373522e333379 100644 --- a/src/plugins/home/server/tutorials/suricata_logs/index.ts +++ b/src/plugins/home/server/tutorials/suricata_logs/index.ts @@ -24,12 +24,12 @@ export function suricataLogsSpecProvider(context: TutorialContext): TutorialSche return { id: 'suricataLogs', name: i18n.translate('home.tutorials.suricataLogs.nameTitle', { - defaultMessage: 'Suricata logs', + defaultMessage: 'Suricata Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.suricataLogs.shortDescription', { - defaultMessage: 'Collect Suricata IDS/IPS/NSM logs.', + defaultMessage: 'Collect and parse logs from Suricata IDS/IPS/NSM with Filebeat.', }), longDescription: i18n.translate('home.tutorials.suricataLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/system_logs/index.ts b/src/plugins/home/server/tutorials/system_logs/index.ts index 6f403a6d0a71a..fcc5745f48252 100644 --- a/src/plugins/home/server/tutorials/system_logs/index.ts +++ b/src/plugins/home/server/tutorials/system_logs/index.ts @@ -24,7 +24,7 @@ export function systemLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'systemLogs', name: i18n.translate('home.tutorials.systemLogs.nameTitle', { - defaultMessage: 'System logs', + defaultMessage: 'System Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, diff --git a/src/plugins/home/server/tutorials/system_metrics/index.ts b/src/plugins/home/server/tutorials/system_metrics/index.ts index 08979a3d3b003..1348535d9bb72 100644 --- a/src/plugins/home/server/tutorials/system_metrics/index.ts +++ b/src/plugins/home/server/tutorials/system_metrics/index.ts @@ -23,16 +23,17 @@ export function systemMetricsSpecProvider(context: TutorialContext): TutorialSch return { id: 'systemMetrics', name: i18n.translate('home.tutorials.systemMetrics.nameTitle', { - defaultMessage: 'System metrics', + defaultMessage: 'System Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.systemMetrics.shortDescription', { - defaultMessage: 'Collect CPU, memory, network, and disk statistics from the host.', + defaultMessage: + 'Collect CPU, memory, network, and disk metrics from System hosts with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.systemMetrics.longDescription', { defaultMessage: - 'The `system` Metricbeat module collects CPU, memory, network, and disk statistics from the host. \ + 'The `system` Metricbeat module collects CPU, memory, network, and disk statistics from host. \ It collects system wide statistics and statistics per process and filesystem. \ [Learn more]({learnMoreLink}).', values: { diff --git a/src/plugins/home/server/tutorials/tomcat_logs/index.ts b/src/plugins/home/server/tutorials/tomcat_logs/index.ts index 5ce4096ad4628..3258d3eff5a16 100644 --- a/src/plugins/home/server/tutorials/tomcat_logs/index.ts +++ b/src/plugins/home/server/tutorials/tomcat_logs/index.ts @@ -24,12 +24,12 @@ export function tomcatLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'tomcatLogs', name: i18n.translate('home.tutorials.tomcatLogs.nameTitle', { - defaultMessage: 'Tomcat logs', + defaultMessage: 'Tomcat Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.tomcatLogs.shortDescription', { - defaultMessage: 'Collect Apache Tomcat logs over syslog or from a file.', + defaultMessage: 'Collect and parse logs from Apache Tomcat servers with Filebeat.', }), longDescription: i18n.translate('home.tutorials.tomcatLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/traefik_logs/index.ts b/src/plugins/home/server/tutorials/traefik_logs/index.ts index 6bbc905bbd6aa..30b9db4022137 100644 --- a/src/plugins/home/server/tutorials/traefik_logs/index.ts +++ b/src/plugins/home/server/tutorials/traefik_logs/index.ts @@ -24,12 +24,12 @@ export function traefikLogsSpecProvider(context: TutorialContext): TutorialSchem return { id: 'traefikLogs', name: i18n.translate('home.tutorials.traefikLogs.nameTitle', { - defaultMessage: 'Traefik logs', + defaultMessage: 'Traefik Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.traefikLogs.shortDescription', { - defaultMessage: 'Collect Traefik access logs.', + defaultMessage: 'Collect and parse logs from Traefik with Filebeat.', }), longDescription: i18n.translate('home.tutorials.traefikLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/traefik_metrics/index.ts b/src/plugins/home/server/tutorials/traefik_metrics/index.ts index 35d54317c8ede..6f76be3056110 100644 --- a/src/plugins/home/server/tutorials/traefik_metrics/index.ts +++ b/src/plugins/home/server/tutorials/traefik_metrics/index.ts @@ -20,16 +20,16 @@ export function traefikMetricsSpecProvider(context: TutorialContext): TutorialSc return { id: 'traefikMetrics', name: i18n.translate('home.tutorials.traefikMetrics.nameTitle', { - defaultMessage: 'Traefik metrics', + defaultMessage: 'Traefik Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.traefikMetrics.shortDescription', { - defaultMessage: 'Fetch monitoring metrics from Traefik.', + defaultMessage: 'Collect metrics from Traefik with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.traefikMetrics.longDescription', { defaultMessage: - 'The `traefik` Metricbeat module fetches monitoring metrics from Traefik. \ + 'The `traefik` Metricbeat module fetches metrics from Traefik. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-traefik.html', diff --git a/src/plugins/home/server/tutorials/uptime_monitors/index.ts b/src/plugins/home/server/tutorials/uptime_monitors/index.ts index 6e949d5410115..118174d0e5717 100644 --- a/src/plugins/home/server/tutorials/uptime_monitors/index.ts +++ b/src/plugins/home/server/tutorials/uptime_monitors/index.ts @@ -28,7 +28,7 @@ export function uptimeMonitorsSpecProvider(context: TutorialContext): TutorialSc moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.uptimeMonitors.shortDescription', { - defaultMessage: 'Monitor services for their availability', + defaultMessage: 'Monitor availability of the services with Heartbeat.', }), longDescription: i18n.translate('home.tutorials.uptimeMonitors.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts b/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts index d9cfcc9f7fb75..b1dbeb89bdb26 100644 --- a/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts +++ b/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts @@ -23,16 +23,16 @@ export function uwsgiMetricsSpecProvider(context: TutorialContext): TutorialSche return { id: 'uwsgiMetrics', name: i18n.translate('home.tutorials.uwsgiMetrics.nameTitle', { - defaultMessage: 'uWSGI metrics', + defaultMessage: 'uWSGI Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.uwsgiMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from the uWSGI server.', + defaultMessage: 'Collect metrics from uWSGI servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.uwsgiMetrics.longDescription', { defaultMessage: - 'The `uwsgi` Metricbeat module fetches internal metrics from the uWSGI server. \ + 'The `uwsgi` Metricbeat module fetches metrics from uWSGI server. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-uwsgi.html', diff --git a/src/plugins/home/server/tutorials/vsphere_metrics/index.ts b/src/plugins/home/server/tutorials/vsphere_metrics/index.ts index bcbcec59c36e4..14a574872221a 100644 --- a/src/plugins/home/server/tutorials/vsphere_metrics/index.ts +++ b/src/plugins/home/server/tutorials/vsphere_metrics/index.ts @@ -23,16 +23,16 @@ export function vSphereMetricsSpecProvider(context: TutorialContext): TutorialSc return { id: 'vsphereMetrics', name: i18n.translate('home.tutorials.vsphereMetrics.nameTitle', { - defaultMessage: 'vSphere metrics', + defaultMessage: 'vSphere Metrics', }), moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.vsphereMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from vSphere.', + defaultMessage: 'Collect metrics from vSphere with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.vsphereMetrics.longDescription', { defaultMessage: - 'The `vsphere` Metricbeat module fetches internal metrics from a vSphere cluster. \ + 'The `vsphere` Metricbeat module fetches metrics from a vSphere cluster. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-vsphere.html', diff --git a/src/plugins/home/server/tutorials/windows_event_logs/index.ts b/src/plugins/home/server/tutorials/windows_event_logs/index.ts index 0df7fa906e085..008468487ea64 100644 --- a/src/plugins/home/server/tutorials/windows_event_logs/index.ts +++ b/src/plugins/home/server/tutorials/windows_event_logs/index.ts @@ -23,17 +23,17 @@ export function windowsEventLogsSpecProvider(context: TutorialContext): Tutorial return { id: 'windowsEventLogs', name: i18n.translate('home.tutorials.windowsEventLogs.nameTitle', { - defaultMessage: 'Windows Event Log', + defaultMessage: 'Windows Event Logs', }), moduleName, isBeta: false, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.windowsEventLogs.shortDescription', { - defaultMessage: 'Fetch logs from the Windows Event Log.', + defaultMessage: 'Collect and parse logs from Windows Event Logs with WinLogBeat.', }), longDescription: i18n.translate('home.tutorials.windowsEventLogs.longDescription', { defaultMessage: - 'Use Winlogbeat to collect the logs from the Windows Event Log. \ + 'Use Winlogbeat to collect the logs from Windows Event Logs. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.winlogbeat}/index.html', diff --git a/src/plugins/home/server/tutorials/windows_metrics/index.ts b/src/plugins/home/server/tutorials/windows_metrics/index.ts index 6c663fbb13d4d..31d9b3f8962ce 100644 --- a/src/plugins/home/server/tutorials/windows_metrics/index.ts +++ b/src/plugins/home/server/tutorials/windows_metrics/index.ts @@ -23,17 +23,17 @@ export function windowsMetricsSpecProvider(context: TutorialContext): TutorialSc return { id: 'windowsMetrics', name: i18n.translate('home.tutorials.windowsMetrics.nameTitle', { - defaultMessage: 'Windows metrics', + defaultMessage: 'Windows Metrics', }), moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.windowsMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from Windows.', + defaultMessage: 'Collect metrics from Windows with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.windowsMetrics.longDescription', { defaultMessage: - 'The `windows` Metricbeat module fetches internal metrics from Windows. \ + 'The `windows` Metricbeat module fetches metrics from Windows. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-windows.html', diff --git a/src/plugins/home/server/tutorials/zeek_logs/index.ts b/src/plugins/home/server/tutorials/zeek_logs/index.ts index 5434dcc8527ff..df86518978c52 100644 --- a/src/plugins/home/server/tutorials/zeek_logs/index.ts +++ b/src/plugins/home/server/tutorials/zeek_logs/index.ts @@ -24,12 +24,12 @@ export function zeekLogsSpecProvider(context: TutorialContext): TutorialSchema { return { id: 'zeekLogs', name: i18n.translate('home.tutorials.zeekLogs.nameTitle', { - defaultMessage: 'Zeek logs', + defaultMessage: 'Zeek Logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.zeekLogs.shortDescription', { - defaultMessage: 'Collect Zeek network security monitoring logs.', + defaultMessage: 'Collect and parse logs from Zeek network security with Filebeat.', }), longDescription: i18n.translate('home.tutorials.zeekLogs.longDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts b/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts index 85ca03acacfd4..8f732969a07f3 100644 --- a/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts +++ b/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts @@ -23,18 +23,18 @@ export function zookeeperMetricsSpecProvider(context: TutorialContext): Tutorial return { id: moduleName + 'Metrics', name: i18n.translate('home.tutorials.zookeeperMetrics.nameTitle', { - defaultMessage: 'Zookeeper metrics', + defaultMessage: 'Zookeeper Metrics', }), moduleName, euiIconType: '/plugins/home/assets/logos/zookeeper.svg', isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.zookeeperMetrics.shortDescription', { - defaultMessage: 'Fetch internal metrics from a Zookeeper server.', + defaultMessage: 'Collect metrics from Zookeeper servers with Metricbeat.', }), longDescription: i18n.translate('home.tutorials.zookeeperMetrics.longDescription', { defaultMessage: - 'The `{moduleName}` Metricbeat module fetches internal metrics from a Zookeeper server. \ + 'The `{moduleName}` Metricbeat module fetches metrics from a Zookeeper server. \ [Learn more]({learnMoreLink}).', values: { moduleName, diff --git a/src/plugins/home/server/tutorials/zscaler_logs/index.ts b/src/plugins/home/server/tutorials/zscaler_logs/index.ts index a2eb41a257a92..977bbb242c62a 100644 --- a/src/plugins/home/server/tutorials/zscaler_logs/index.ts +++ b/src/plugins/home/server/tutorials/zscaler_logs/index.ts @@ -29,7 +29,7 @@ export function zscalerLogsSpecProvider(context: TutorialContext): TutorialSchem moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.zscalerLogs.shortDescription', { - defaultMessage: 'This is a module for receiving Zscaler NSS logs over Syslog or a file.', + defaultMessage: 'Collect and parse logs from Zscaler NSS with Filebeat.', }), longDescription: i18n.translate('home.tutorials.zscalerLogs.longDescription', { defaultMessage: diff --git a/x-pack/plugins/apm/server/tutorial/index.ts b/x-pack/plugins/apm/server/tutorial/index.ts index a3ad59d842df1..66ff8f5b2c92c 100644 --- a/x-pack/plugins/apm/server/tutorial/index.ts +++ b/x-pack/plugins/apm/server/tutorial/index.ts @@ -22,7 +22,7 @@ import apmDataView from './index_pattern.json'; const apmIntro = i18n.translate('xpack.apm.tutorial.introduction', { defaultMessage: - 'Collect in-depth performance metrics and errors from inside your applications.', + 'Collect performance metrics from your applications with Elastic APM.', }); const moduleName = 'apm'; diff --git a/x-pack/plugins/maps/server/tutorials/ems/index.ts b/x-pack/plugins/maps/server/tutorials/ems/index.ts index ba8720a7bc8eb..47cb5476c4b90 100644 --- a/x-pack/plugins/maps/server/tutorials/ems/index.ts +++ b/x-pack/plugins/maps/server/tutorials/ems/index.ts @@ -65,7 +65,7 @@ export function emsBoundariesSpecProvider({ }), category: TutorialsCategory.OTHER, shortDescription: i18n.translate('xpack.maps.tutorials.ems.shortDescription', { - defaultMessage: 'Administrative boundaries from the Elastic Maps Service.', + defaultMessage: 'Add administrative boundaries to your data with Elastic Maps Service.', }), longDescription: i18n.translate('xpack.maps.tutorials.ems.longDescription', { defaultMessage: From 4b0ae9633cc8c3d729c2940d3759fb00ff78987c Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Thu, 21 Oct 2021 14:57:30 -0400 Subject: [PATCH 25/40] [Security Solution][Endpoint] un-skip permissions FTR tests for endpoint (#115962) * unskip test suite and skip the Test for Host details - found bug --- .../apps/endpoint/endpoint_permissions.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_permissions.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_permissions.ts index 90dd5123f5d36..48c0aea825048 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_permissions.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_permissions.ts @@ -20,8 +20,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const endpointTestResources = getService('endpointTestResources'); const policyTestResources = getService('policyTestResources'); - // failing ES promotion: https://github.com/elastic/kibana/issues/110309 - describe.skip('Endpoint permissions:', () => { + describe('Endpoint permissions:', () => { let indexedData: IndexedHostsAndAlertsResponse; before(async () => { @@ -62,7 +61,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.existOrFail('noIngestPermissions'); }); - it('should display endpoint data on Host Details', async () => { + // FIXME:PT skipped. need to fix security-team bug #1929 + it.skip('should display endpoint data on Host Details', async () => { const endpoint = indexedData.hosts[0]; await PageObjects.hosts.navigateToHostDetails(endpoint.host.name); const endpointSummary = await PageObjects.hosts.hostDetailsEndpointOverviewData(); From 6344b8d2a49430aabd1ca1f57cea98896fb2c9a8 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 21 Oct 2021 12:19:34 -0700 Subject: [PATCH 26/40] [Alerting] Changed alerting telemetry throttle and schedule time metrics to be numbers instead of strings (#115605) * [Alerting] Changed alerting telemetry throttle and interval metrics to be numbers instead of strings * fixed test * fixed typecheck * set size to 0 Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../actions/server/usage/actions_telemetry.ts | 2 + .../server/usage/alerts_telemetry.test.ts | 66 ++++++++++++++++++- .../alerting/server/usage/alerts_telemetry.ts | 20 +++--- .../server/usage/alerts_usage_collector.ts | 24 +++---- x-pack/plugins/alerting/server/usage/types.ts | 12 ++-- .../schema/xpack_plugins.json | 12 ++-- 6 files changed, 101 insertions(+), 35 deletions(-) diff --git a/x-pack/plugins/actions/server/usage/actions_telemetry.ts b/x-pack/plugins/actions/server/usage/actions_telemetry.ts index 1cb6bf8bfc74c..803a2122fe7f8 100644 --- a/x-pack/plugins/actions/server/usage/actions_telemetry.ts +++ b/x-pack/plugins/actions/server/usage/actions_telemetry.ts @@ -43,6 +43,7 @@ export async function getTotalCount( const { body: searchResult } = await esClient.search({ index: kibanaIndex, + size: 0, body: { query: { bool: { @@ -224,6 +225,7 @@ export async function getInUseTotalCount( const { body: actionResults } = await esClient.search({ index: kibanaIndex, + size: 0, body: { query: { bool: { diff --git a/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts b/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts index 15fa6e63ac561..348036252817d 100644 --- a/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts +++ b/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts @@ -7,7 +7,7 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from '../../../../../src/core/server/elasticsearch/client/mocks'; -import { getTotalCountInUse } from './alerts_telemetry'; +import { getTotalCountAggregations, getTotalCountInUse } from './alerts_telemetry'; describe('alerts telemetry', () => { test('getTotalCountInUse should replace first "." symbol to "__" in alert types names', async () => { @@ -49,6 +49,70 @@ Object { "countNamespaces": 1, "countTotal": 4, } +`); + }); + + test('getTotalCountAggregations should return aggregations for throttle, interval and associated actions', async () => { + const mockEsClient = elasticsearchClientMock.createClusterClient().asScoped().asInternalUser; + mockEsClient.search.mockReturnValue( + // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + elasticsearchClientMock.createSuccessTransportRequestPromise({ + aggregations: { + byAlertTypeId: { + value: { + ruleTypes: { + '.index-threshold': 2, + 'logs.alert.document.count': 1, + 'document.test.': 1, + }, + namespaces: { + default: 1, + }, + }, + }, + throttleTime: { value: { min: 0, max: 10, totalCount: 10, totalSum: 20 } }, + intervalTime: { value: { min: 0, max: 2, totalCount: 2, totalSum: 5 } }, + connectorsAgg: { + connectors: { + value: { min: 0, max: 5, totalActionsCount: 10, totalAlertsCount: 2 }, + }, + }, + }, + hits: { + hits: [], + }, + }) + ); + + const telemetry = await getTotalCountAggregations(mockEsClient, 'test'); + + expect(mockEsClient.search).toHaveBeenCalledTimes(1); + + expect(telemetry).toMatchInlineSnapshot(` +Object { + "connectors_per_alert": Object { + "avg": 2.5, + "max": 5, + "min": 0, + }, + "count_by_type": Object { + "__index-threshold": 2, + "document.test__": 1, + "logs.alert.document.count": 1, + }, + "count_rules_namespaces": 0, + "count_total": 4, + "schedule_time": Object { + "avg": 2.5, + "max": 2, + "min": 0, + }, + "throttle_time": Object { + "avg": 2, + "max": 10, + "min": 0, + }, +} `); }); }); diff --git a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts b/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts index 18fa9b590b4e1..ede2ac3613296 100644 --- a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts +++ b/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts @@ -233,6 +233,7 @@ export async function getTotalCountAggregations( const { body: results } = await esClient.search({ index: kibanaInex, + size: 0, body: { query: { bool: { @@ -284,22 +285,20 @@ export async function getTotalCountAggregations( {} ), throttle_time: { - min: `${aggregations.throttleTime.value.min}s`, - avg: `${ + min: aggregations.throttleTime.value.min, + avg: aggregations.throttleTime.value.totalCount > 0 ? aggregations.throttleTime.value.totalSum / aggregations.throttleTime.value.totalCount - : 0 - }s`, - max: `${aggregations.throttleTime.value.max}s`, + : 0, + max: aggregations.throttleTime.value.max, }, schedule_time: { - min: `${aggregations.intervalTime.value.min}s`, - avg: `${ + min: aggregations.intervalTime.value.min, + avg: aggregations.intervalTime.value.totalCount > 0 ? aggregations.intervalTime.value.totalSum / aggregations.intervalTime.value.totalCount - : 0 - }s`, - max: `${aggregations.intervalTime.value.max}s`, + : 0, + max: aggregations.intervalTime.value.max, }, connectors_per_alert: { min: aggregations.connectorsAgg.connectors.value.min, @@ -316,6 +315,7 @@ export async function getTotalCountAggregations( export async function getTotalCountInUse(esClient: ElasticsearchClient, kibanaInex: string) { const { body: searchResult } = await esClient.search({ index: kibanaInex, + size: 0, body: { query: { bool: { diff --git a/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts b/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts index ecea721dfad92..e9405c51dbf15 100644 --- a/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts +++ b/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts @@ -75,14 +75,14 @@ export function createAlertsUsageCollector( count_active_total: 0, count_disabled_total: 0, throttle_time: { - min: '0s', - avg: '0s', - max: '0s', + min: 0, + avg: 0, + max: 0, }, schedule_time: { - min: '0s', - avg: '0s', - max: '0s', + min: 0, + avg: 0, + max: 0, }, connectors_per_alert: { min: 0, @@ -100,14 +100,14 @@ export function createAlertsUsageCollector( count_active_total: { type: 'long' }, count_disabled_total: { type: 'long' }, throttle_time: { - min: { type: 'keyword' }, - avg: { type: 'keyword' }, - max: { type: 'keyword' }, + min: { type: 'long' }, + avg: { type: 'float' }, + max: { type: 'long' }, }, schedule_time: { - min: { type: 'keyword' }, - avg: { type: 'keyword' }, - max: { type: 'keyword' }, + min: { type: 'long' }, + avg: { type: 'float' }, + max: { type: 'long' }, }, connectors_per_alert: { min: { type: 'long' }, diff --git a/x-pack/plugins/alerting/server/usage/types.ts b/x-pack/plugins/alerting/server/usage/types.ts index 5e420b54e37cb..0e489893a1bbc 100644 --- a/x-pack/plugins/alerting/server/usage/types.ts +++ b/x-pack/plugins/alerting/server/usage/types.ts @@ -13,14 +13,14 @@ export interface AlertsUsage { count_active_by_type: Record; count_rules_namespaces: number; throttle_time: { - min: string; - avg: string; - max: string; + min: number; + avg: number; + max: number; }; schedule_time: { - min: string; - avg: string; - max: string; + min: number; + avg: number; + max: number; }; connectors_per_alert: { min: number; diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 12763e4e26e31..b3ca5f17634d5 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -144,26 +144,26 @@ "throttle_time": { "properties": { "min": { - "type": "keyword" + "type": "long" }, "avg": { - "type": "keyword" + "type": "float" }, "max": { - "type": "keyword" + "type": "long" } } }, "schedule_time": { "properties": { "min": { - "type": "keyword" + "type": "long" }, "avg": { - "type": "keyword" + "type": "float" }, "max": { - "type": "keyword" + "type": "long" } } }, From 21878b5b542919f5f99f94c1583eaa5c9da5737b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Thu, 21 Oct 2021 15:43:22 -0400 Subject: [PATCH 27/40] [APM] Dark metadata icons on service overview page (#115964) --- .../apm/public/components/shared/service_icons/icon_popover.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/public/components/shared/service_icons/icon_popover.tsx b/x-pack/plugins/apm/public/components/shared/service_icons/icon_popover.tsx index 695c941c61ed4..19abd2059c903 100644 --- a/x-pack/plugins/apm/public/components/shared/service_icons/icon_popover.tsx +++ b/x-pack/plugins/apm/public/components/shared/service_icons/icon_popover.tsx @@ -50,7 +50,7 @@ export function IconPopover({ } From ce546998a43be795a0a5115cdb7b0af611e9c302 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Thu, 21 Oct 2021 15:44:06 -0400 Subject: [PATCH 28/40] Add deprecation levels (#115832) --- x-pack/plugins/actions/server/index.ts | 7 ++++++- x-pack/plugins/alerting/server/index.ts | 8 +++++--- x-pack/plugins/stack_alerts/server/index.ts | 1 + x-pack/plugins/task_manager/server/index.ts | 2 ++ 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index 14cdfacd360a2..e6c82969a0aa2 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -57,7 +57,9 @@ export const plugin = (initContext: PluginInitializerContext) => new ActionsPlug export const config: PluginConfigDescriptor = { schema: configSchema, deprecations: ({ renameFromRoot, unused }) => [ - renameFromRoot('xpack.actions.whitelistedHosts', 'xpack.actions.allowedHosts'), + renameFromRoot('xpack.actions.whitelistedHosts', 'xpack.actions.allowedHosts', { + level: 'warning', + }), (settings, fromPath, addDeprecation) => { const actions = get(settings, fromPath); const customHostSettings = actions?.customHostSettings ?? []; @@ -69,6 +71,7 @@ export const config: PluginConfigDescriptor = { ) ) { addDeprecation({ + level: 'warning', configPath: 'xpack.actions.customHostSettings.ssl.rejectUnauthorized', message: `"xpack.actions.customHostSettings[].ssl.rejectUnauthorized" is deprecated.` + @@ -97,6 +100,7 @@ export const config: PluginConfigDescriptor = { const actions = get(settings, fromPath); if (actions?.hasOwnProperty('rejectUnauthorized')) { addDeprecation({ + level: 'warning', configPath: `${fromPath}.rejectUnauthorized`, message: `"xpack.actions.rejectUnauthorized" is deprecated. Use "xpack.actions.verificationMode" instead, ` + @@ -124,6 +128,7 @@ export const config: PluginConfigDescriptor = { const actions = get(settings, fromPath); if (actions?.hasOwnProperty('proxyRejectUnauthorizedCertificates')) { addDeprecation({ + level: 'warning', configPath: `${fromPath}.proxyRejectUnauthorizedCertificates`, message: `"xpack.actions.proxyRejectUnauthorizedCertificates" is deprecated. Use "xpack.actions.proxyVerificationMode" instead, ` + diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts index 162ee06216304..2ddb6ff711c46 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/alerting/server/index.ts @@ -48,14 +48,16 @@ export const plugin = (initContext: PluginInitializerContext) => new AlertingPlu export const config: PluginConfigDescriptor = { schema: configSchema, deprecations: ({ renameFromRoot }) => [ - renameFromRoot('xpack.alerts.healthCheck', 'xpack.alerting.healthCheck'), + renameFromRoot('xpack.alerts.healthCheck', 'xpack.alerting.healthCheck', { level: 'warning' }), renameFromRoot( 'xpack.alerts.invalidateApiKeysTask.interval', - 'xpack.alerting.invalidateApiKeysTask.interval' + 'xpack.alerting.invalidateApiKeysTask.interval', + { level: 'warning' } ), renameFromRoot( 'xpack.alerts.invalidateApiKeysTask.removalDelay', - 'xpack.alerting.invalidateApiKeysTask.removalDelay' + 'xpack.alerting.invalidateApiKeysTask.removalDelay', + { level: 'warning' } ), ], }; diff --git a/x-pack/plugins/stack_alerts/server/index.ts b/x-pack/plugins/stack_alerts/server/index.ts index 1ac774a2d6c3f..b6b117ceb7075 100644 --- a/x-pack/plugins/stack_alerts/server/index.ts +++ b/x-pack/plugins/stack_alerts/server/index.ts @@ -18,6 +18,7 @@ export const config: PluginConfigDescriptor = { const stackAlerts = get(settings, fromPath); if (stackAlerts?.enabled === false || stackAlerts?.enabled === true) { addDeprecation({ + level: 'critical', configPath: 'xpack.stack_alerts.enabled', message: `"xpack.stack_alerts.enabled" is deprecated. The ability to disable this plugin will be removed in 8.0.0.`, correctiveActions: { diff --git a/x-pack/plugins/task_manager/server/index.ts b/x-pack/plugins/task_manager/server/index.ts index 100f95729d1b7..d078c7b78ad94 100644 --- a/x-pack/plugins/task_manager/server/index.ts +++ b/x-pack/plugins/task_manager/server/index.ts @@ -49,6 +49,7 @@ export const config: PluginConfigDescriptor = { const taskManager = get(settings, fromPath); if (taskManager?.index) { addDeprecation({ + level: 'critical', configPath: `${fromPath}.index`, documentationUrl: 'https://ela.st/kbn-remove-legacy-multitenancy', message: `"${fromPath}.index" is deprecated. Multitenancy by changing "kibana.index" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details`, @@ -62,6 +63,7 @@ export const config: PluginConfigDescriptor = { } if (taskManager?.max_workers > MAX_WORKERS_LIMIT) { addDeprecation({ + level: 'critical', configPath: `${fromPath}.max_workers`, message: `setting "${fromPath}.max_workers" (${taskManager?.max_workers}) greater than ${MAX_WORKERS_LIMIT} is deprecated. Values greater than ${MAX_WORKERS_LIMIT} will not be supported starting in 8.0.`, correctiveActions: { From 29e5a3a37f6517f1f5ea4237c623433ef3c5d319 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 21 Oct 2021 16:19:17 -0400 Subject: [PATCH 29/40] [FLEET] Increase asset size limit for installing ML model packages (#115890) * update size limit for assets to 50mb * use different size limit for ml model * add byte constant for clarity --- .../server/services/epm/archive/storage.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/epm/archive/storage.ts b/x-pack/plugins/fleet/server/services/epm/archive/storage.ts index d3bc4afae6229..29594b0a247a1 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/storage.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/storage.ts @@ -29,8 +29,11 @@ import { getArchiveEntry, setArchiveEntry, setArchiveFilelist, setPackageInfo } import type { ArchiveEntry } from './index'; import { parseAndVerifyPolicyTemplates, parseAndVerifyStreams } from './validation'; +const ONE_BYTE = 1024 * 1024; // could be anything, picked this from https://github.com/elastic/elastic-agent-client/issues/17 -const MAX_ES_ASSET_BYTES = 4 * 1024 * 1024; +const MAX_ES_ASSET_BYTES = 4 * ONE_BYTE; +// Updated to accomodate larger package size in some ML model packages +const ML_MAX_ES_ASSET_BYTES = 50 * ONE_BYTE; export interface PackageAsset { package_name: string; @@ -64,15 +67,20 @@ export async function archiveEntryToESDocument(opts: { const bufferIsBinary = await isBinaryFile(buffer); const dataUtf8 = bufferIsBinary ? '' : buffer.toString('utf8'); const dataBase64 = bufferIsBinary ? buffer.toString('base64') : ''; + const currentMaxAssetBytes = path.includes('ml_model') + ? ML_MAX_ES_ASSET_BYTES + : MAX_ES_ASSET_BYTES; // validation: filesize? asset type? anything else - if (dataUtf8.length > MAX_ES_ASSET_BYTES) { - throw new Error(`File at ${path} is larger than maximum allowed size of ${MAX_ES_ASSET_BYTES}`); + if (dataUtf8.length > currentMaxAssetBytes) { + throw new Error( + `File at ${path} is larger than maximum allowed size of ${currentMaxAssetBytes}` + ); } - if (dataBase64.length > MAX_ES_ASSET_BYTES) { + if (dataBase64.length > currentMaxAssetBytes) { throw new Error( - `After base64 encoding file at ${path} is larger than maximum allowed size of ${MAX_ES_ASSET_BYTES}` + `After base64 encoding file at ${path} is larger than maximum allowed size of ${currentMaxAssetBytes}` ); } From 00f05ce207bba11a3fd967f432151a1c9aef2312 Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Thu, 21 Oct 2021 15:23:35 -0500 Subject: [PATCH 30/40] [Stack Monitoring] Fix missing breadcrumbs to logstash (#115780) --- .../application/pages/logstash/advanced.tsx | 17 +++++++++++++++-- .../public/application/pages/logstash/node.tsx | 16 ++++++++++++++-- .../pages/logstash/node_pipelines.tsx | 17 +++++++++++++++-- .../public/application/pages/logstash/nodes.tsx | 14 ++++++++++++-- .../application/pages/logstash/overview.tsx | 14 ++++++++++++-- .../application/pages/logstash/pipeline.tsx | 13 ++++++++++++- .../application/pages/logstash/pipelines.tsx | 15 +++++++++++++-- 7 files changed, 93 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/advanced.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/advanced.tsx index 3532a24f3da37..abff02ef7b3c3 100644 --- a/x-pack/plugins/monitoring/public/application/pages/logstash/advanced.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/advanced.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useContext, useState, useCallback, useMemo } from 'react'; +import React, { useContext, useState, useCallback, useMemo, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { @@ -32,6 +32,7 @@ import { useCharts } from '../../hooks/use_charts'; import { AlertsByName } from '../../../alerts/types'; import { fetchAlerts } from '../../../lib/fetch_alerts'; import { RULE_LOGSTASH_VERSION_MISMATCH } from '../../../../common/constants'; +import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; export const LogStashNodeAdvancedPage: React.FC = ({ clusters }) => { const globalState = useContext(GlobalStateContext); @@ -42,7 +43,9 @@ export const LogStashNodeAdvancedPage: React.FC = ({ clusters }) const ccs = globalState.ccs; const cluster = find(clusters, { cluster_uuid: clusterUuid, - }); + }) as any; + + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); const [data, setData] = useState({} as any); const [alerts, setAlerts] = useState({}); @@ -108,6 +111,16 @@ export const LogStashNodeAdvancedPage: React.FC = ({ clusters }) ]; }, [data.metrics]); + useEffect(() => { + if (cluster && data.nodeSummary) { + generateBreadcrumbs(cluster.cluster_name, { + inLogstash: true, + instance: data.nodeSummary.host, + name: 'nodes', + }); + } + }, [cluster, data, generateBreadcrumbs]); + return ( = ({ clusters }) => { const match = useRouteMatch<{ uuid: string | undefined }>(); const globalState = useContext(GlobalStateContext); const { services } = useKibana<{ data: any }>(); + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); const clusterUuid = globalState.cluster_uuid; const ccs = globalState.ccs; const cluster = find(clusters, { cluster_uuid: clusterUuid, - }); + }) as any; const [data, setData] = useState({} as any); const [alerts, setAlerts] = useState({}); const { zoomInfo, onBrush } = useCharts(); @@ -60,6 +62,16 @@ export const LogStashNodePage: React.FC = ({ clusters }) => { }, }); + useEffect(() => { + if (cluster && data.nodeSummary) { + generateBreadcrumbs(cluster.cluster_name, { + inLogstash: true, + instance: data.nodeSummary.host, + name: 'nodes', + }); + } + }, [cluster, data, generateBreadcrumbs]); + const getPageData = useCallback(async () => { const url = `../api/monitoring/v1/clusters/${clusterUuid}/logstash/node/${match.params.uuid}`; const bounds = services.data?.query.timefilter.timefilter.getBounds(); diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/node_pipelines.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/node_pipelines.tsx index 740202da57d24..0d89adeaeadd7 100644 --- a/x-pack/plugins/monitoring/public/application/pages/logstash/node_pipelines.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/node_pipelines.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useContext, useState, useCallback } from 'react'; +import React, { useContext, useState, useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; // @ts-ignore @@ -25,6 +25,7 @@ import { useTable } from '../../hooks/use_table'; // @ts-ignore import { PipelineListing } from '../../../components/logstash/pipeline_listing/pipeline_listing'; import { useCharts } from '../../hooks/use_charts'; +import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; export const LogStashNodePipelinesPage: React.FC = ({ clusters }) => { const globalState = useContext(GlobalStateContext); @@ -35,7 +36,9 @@ export const LogStashNodePipelinesPage: React.FC = ({ clusters } const { onBrush, zoomInfo } = useCharts(); const cluster = find(clusters, { cluster_uuid: clusterUuid, - }); + }) as any; + + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); const { getPaginationTableProps, getPaginationRouteOptions, updateTotalItemCount } = useTable('logstash.pipelines'); @@ -83,6 +86,16 @@ export const LogStashNodePipelinesPage: React.FC = ({ clusters } match.params.uuid, ]); + useEffect(() => { + if (cluster && data.nodeSummary) { + generateBreadcrumbs(cluster.cluster_name, { + inLogstash: true, + instance: data.nodeSummary.host, + name: 'nodes', + }); + } + }, [cluster, data, generateBreadcrumbs]); + return ( = ({ clusters }) => { const { services } = useKibana<{ data: any }>(); const clusterUuid = globalState.cluster_uuid; const ccs = globalState.ccs; + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); const cluster = find(clusters, { cluster_uuid: clusterUuid, - }); + }) as any; const [data, setData] = useState({} as any); const [alerts, setAlerts] = useState({}); const { getPaginationTableProps } = useTable('logstash.nodes'); @@ -75,6 +77,14 @@ export const LogStashNodesPage: React.FC = ({ clusters }) => { } }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http]); + useEffect(() => { + if (cluster) { + generateBreadcrumbs(cluster.cluster_name, { + inLogstash: true, + }); + } + }, [cluster, generateBreadcrumbs]); + return ( = ({ clusters }) => { const globalState = useContext(GlobalStateContext); @@ -23,7 +24,8 @@ export const LogStashOverviewPage: React.FC = ({ clusters }) => const ccs = globalState.ccs; const cluster = find(clusters, { cluster_uuid: clusterUuid, - }); + }) as any; + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); const [data, setData] = useState(null); // const [showShardActivityHistory, setShowShardActivityHistory] = useState(false); @@ -53,6 +55,14 @@ export const LogStashOverviewPage: React.FC = ({ clusters }) => setData(response); }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http]); + useEffect(() => { + if (cluster) { + generateBreadcrumbs(cluster.cluster_name, { + inLogstash: true, + }); + } + }, [cluster, data, generateBreadcrumbs]); + const renderOverview = (overviewData: any) => { if (overviewData === null) { return null; diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline.tsx index 20f1caee2b1d8..1f56ea22839e2 100644 --- a/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline.tsx @@ -29,6 +29,7 @@ import { formatTimestampToDuration } from '../../../../common'; import { CALCULATE_DURATION_SINCE } from '../../../../common/constants'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { PipelineVersions } from './pipeline_versions_dropdown'; +import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; export const LogStashPipelinePage: React.FC = ({ clusters }) => { const match = useRouteMatch<{ id: string | undefined; hash: string | undefined }>(); @@ -43,7 +44,7 @@ export const LogStashPipelinePage: React.FC = ({ clusters }) => const ccs = globalState.ccs; const cluster = find(clusters, { cluster_uuid: clusterUuid, - }); + }) as any; const [data, setData] = useState({} as any); const [detailVertexId, setDetailVertexId] = useState(null); const { updateTotalItemCount } = useTable('logstash.pipelines'); @@ -125,6 +126,7 @@ export const LogStashPipelinePage: React.FC = ({ clusters }) => }, [data]); const timeseriesTooltipXValueFormatter = (xValue: any) => moment(xValue).format(dateFormat); + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); const onVertexChange = useCallback( (vertex: any) => { @@ -145,6 +147,15 @@ export const LogStashPipelinePage: React.FC = ({ clusters }) => ); }, [pipelineId, pipelineHash]); + useEffect(() => { + if (cluster) { + generateBreadcrumbs(cluster.cluster_name, { + inLogstash: true, + page: 'pipeline', + }); + } + }, [cluster, data, generateBreadcrumbs]); + return ( = ({ clusters }) => { const globalState = useContext(GlobalStateContext); @@ -29,7 +30,7 @@ export const LogStashPipelinesPage: React.FC = ({ clusters }) => const cluster = find(clusters, { cluster_uuid: clusterUuid, - }); + }) as any; const [data, setData] = useState(null); const { getPaginationTableProps, getPaginationRouteOptions, updateTotalItemCount } = useTable('logstash.pipelines'); @@ -42,6 +43,8 @@ export const LogStashPipelinesPage: React.FC = ({ clusters }) => defaultMessage: 'Logstash pipelines', }); + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); + const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/logstash/pipelines`; @@ -69,6 +72,14 @@ export const LogStashPipelinesPage: React.FC = ({ clusters }) => updateTotalItemCount, ]); + useEffect(() => { + if (cluster) { + generateBreadcrumbs(cluster.cluster_name, { + inLogstash: true, + }); + } + }, [cluster, data, generateBreadcrumbs]); + const renderOverview = (pageData: any) => { if (pageData === null) { return null; From c11e228b3d43eefc0f81b0f84f149bde7494a0aa Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Thu, 21 Oct 2021 16:36:36 -0400 Subject: [PATCH 31/40] [Security Solution] Adds missing migration script execution for Endpoint policy (#115969) --- .../fleet/server/saved_objects/index.ts | 3 ++- .../saved_objects/migrations/to_v7_16_0.ts | 18 +++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index f0b51b19dda33..e8fda952f17e6 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -44,7 +44,7 @@ import { } from './migrations/to_v7_13_0'; import { migratePackagePolicyToV7140, migrateInstallationToV7140 } from './migrations/to_v7_14_0'; import { migratePackagePolicyToV7150 } from './migrations/to_v7_15_0'; -import { migrateInstallationToV7160 } from './migrations/to_v7_16_0'; +import { migrateInstallationToV7160, migratePackagePolicyToV7160 } from './migrations/to_v7_16_0'; /* * Saved object types and mappings @@ -294,6 +294,7 @@ const getSavedObjectTypes = ( '7.13.0': migratePackagePolicyToV7130, '7.14.0': migratePackagePolicyToV7140, '7.15.0': migratePackagePolicyToV7150, + '7.16.0': migratePackagePolicyToV7160, }, }, [PACKAGES_SAVED_OBJECT_TYPE]: { diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_16_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_16_0.ts index 7d12c550ec406..b69523434408b 100644 --- a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_16_0.ts +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_16_0.ts @@ -7,9 +7,11 @@ import type { SavedObjectMigrationFn } from 'kibana/server'; -import type { Installation } from '../../../common'; +import type { Installation, PackagePolicy } from '../../../common'; import { AUTO_UPDATE_PACKAGES, DEFAULT_PACKAGES } from '../../../common'; +import { migratePackagePolicyToV7160 as SecSolMigratePackagePolicyToV7160 } from './security_solution'; + export const migrateInstallationToV7160: SavedObjectMigrationFn = ( installationDoc, migrationContext @@ -26,3 +28,17 @@ export const migrateInstallationToV7160: SavedObjectMigrationFn = ( + packagePolicyDoc, + migrationContext +) => { + let updatedPackagePolicyDoc = packagePolicyDoc; + + // Endpoint specific migrations + if (packagePolicyDoc.attributes.package?.name === 'endpoint') { + updatedPackagePolicyDoc = SecSolMigratePackagePolicyToV7160(packagePolicyDoc, migrationContext); + } + + return updatedPackagePolicyDoc; +}; From eeb005510c1b31c646fcedfcd624e3f67dc64e76 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Thu, 21 Oct 2021 22:38:19 +0200 Subject: [PATCH 32/40] [Lens] cleanup divide mock file to smaller pieces (#115925) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/lens/public/mocks.tsx | 540 ------------------ .../lens/public/mocks/data_plugin_mock.ts | 122 ++++ .../lens/public/mocks/datasource_mock.ts | 78 +++ .../public/mocks/expression_renderer_mock.tsx | 16 + x-pack/plugins/lens/public/mocks/index.ts | 42 ++ .../lens/public/mocks/lens_plugin_mock.tsx | 31 + .../lens/public/mocks/services_mock.tsx | 143 +++++ .../plugins/lens/public/mocks/store_mocks.tsx | 140 +++++ .../lens/public/mocks/visualization_mock.ts | 63 ++ 9 files changed, 635 insertions(+), 540 deletions(-) delete mode 100644 x-pack/plugins/lens/public/mocks.tsx create mode 100644 x-pack/plugins/lens/public/mocks/data_plugin_mock.ts create mode 100644 x-pack/plugins/lens/public/mocks/datasource_mock.ts create mode 100644 x-pack/plugins/lens/public/mocks/expression_renderer_mock.tsx create mode 100644 x-pack/plugins/lens/public/mocks/index.ts create mode 100644 x-pack/plugins/lens/public/mocks/lens_plugin_mock.tsx create mode 100644 x-pack/plugins/lens/public/mocks/services_mock.tsx create mode 100644 x-pack/plugins/lens/public/mocks/store_mocks.tsx create mode 100644 x-pack/plugins/lens/public/mocks/visualization_mock.ts diff --git a/x-pack/plugins/lens/public/mocks.tsx b/x-pack/plugins/lens/public/mocks.tsx deleted file mode 100644 index 5c285f70b2ed9..0000000000000 --- a/x-pack/plugins/lens/public/mocks.tsx +++ /dev/null @@ -1,540 +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'; -// eslint-disable-next-line import/no-extraneous-dependencies -import { ReactWrapper } from 'enzyme'; -// eslint-disable-next-line import/no-extraneous-dependencies -import { mountWithIntl as mount } from '@kbn/test/jest'; -import { Observable, Subject } from 'rxjs'; -import { coreMock } from 'src/core/public/mocks'; -import moment from 'moment'; -import { Provider } from 'react-redux'; -import { act } from 'react-dom/test-utils'; -import { ReactExpressionRendererProps } from 'src/plugins/expressions/public'; -import { PreloadedState } from '@reduxjs/toolkit'; -import { LensPublicStart } from '.'; -import { visualizationTypes } from './xy_visualization/types'; -import { navigationPluginMock } from '../../../../src/plugins/navigation/public/mocks'; -import { LensAppServices } from './app_plugin/types'; -import { DOC_TYPE, layerTypes } from '../common'; -import { DataPublicPluginStart, esFilters, UI_SETTINGS } from '../../../../src/plugins/data/public'; -import { inspectorPluginMock } from '../../../../src/plugins/inspector/public/mocks'; -import { spacesPluginMock } from '../../spaces/public/mocks'; -import { dashboardPluginMock } from '../../../../src/plugins/dashboard/public/mocks'; -import type { - LensByValueInput, - LensByReferenceInput, - ResolvedLensSavedObjectAttributes, -} from './embeddable/embeddable'; -import { - mockAttributeService, - createEmbeddableStateTransferMock, -} from '../../../../src/plugins/embeddable/public/mocks'; -import { fieldFormatsServiceMock } from '../../../../src/plugins/field_formats/public/mocks'; -import type { LensAttributeService } from './lens_attribute_service'; -import type { EmbeddableStateTransfer } from '../../../../src/plugins/embeddable/public'; - -import { - makeConfigureStore, - LensAppState, - LensState, - LensStoreDeps, -} from './state_management/index'; -import { getResolvedDateRange } from './utils'; -import { presentationUtilPluginMock } from '../../../../src/plugins/presentation_util/public/mocks'; -import { - DatasourcePublicAPI, - Datasource, - Visualization, - FramePublicAPI, - FrameDatasourceAPI, - DatasourceMap, - VisualizationMap, -} from './types'; -import { getLensInspectorService } from './lens_inspector_service'; - -export function mockDatasourceStates() { - return { - testDatasource: { - state: {}, - isLoading: false, - }, - }; -} - -export function createMockVisualization(id = 'testVis'): jest.Mocked { - return { - id, - clearLayer: jest.fn((state, _layerId) => state), - removeLayer: jest.fn(), - getLayerIds: jest.fn((_state) => ['layer1']), - getSupportedLayers: jest.fn(() => [{ type: layerTypes.DATA, label: 'Data Layer' }]), - getLayerType: jest.fn((_state, _layerId) => layerTypes.DATA), - visualizationTypes: [ - { - icon: 'empty', - id, - label: 'TEST', - groupLabel: `${id}Group`, - }, - ], - appendLayer: jest.fn(), - getVisualizationTypeId: jest.fn((_state) => 'empty'), - getDescription: jest.fn((_state) => ({ label: '' })), - switchVisualizationType: jest.fn((_, x) => x), - getSuggestions: jest.fn((_options) => []), - initialize: jest.fn((_frame, _state?) => ({ newState: 'newState' })), - getConfiguration: jest.fn((props) => ({ - groups: [ - { - groupId: 'a', - groupLabel: 'a', - layerId: 'layer1', - supportsMoreColumns: true, - accessors: [], - filterOperations: jest.fn(() => true), - dataTestSubj: 'mockVisA', - }, - ], - })), - toExpression: jest.fn((_state, _frame) => null), - toPreviewExpression: jest.fn((_state, _frame) => null), - - setDimension: jest.fn(), - removeDimension: jest.fn(), - getErrorMessages: jest.fn((_state) => undefined), - renderDimensionEditor: jest.fn(), - }; -} - -export const visualizationMap = { - testVis: createMockVisualization(), - testVis2: createMockVisualization(), -}; - -export type DatasourceMock = jest.Mocked & { - publicAPIMock: jest.Mocked; -}; - -export function createMockDatasource(id: string): DatasourceMock { - const publicAPIMock: jest.Mocked = { - datasourceId: id, - getTableSpec: jest.fn(() => []), - getOperationForColumnId: jest.fn(), - }; - - return { - id: 'testDatasource', - clearLayer: jest.fn((state, _layerId) => state), - getDatasourceSuggestionsForField: jest.fn((_state, _item, filterFn) => []), - getDatasourceSuggestionsForVisualizeField: jest.fn((_state, _indexpatternId, _fieldName) => []), - getDatasourceSuggestionsFromCurrentState: jest.fn((_state) => []), - getPersistableState: jest.fn((x) => ({ - state: x, - savedObjectReferences: [{ type: 'index-pattern', id: 'mockip', name: 'mockip' }], - })), - getPublicAPI: jest.fn().mockReturnValue(publicAPIMock), - initialize: jest.fn((_state?) => Promise.resolve()), - renderDataPanel: jest.fn(), - renderLayerPanel: jest.fn(), - toExpression: jest.fn((_frame, _state) => null), - insertLayer: jest.fn((_state, _newLayerId) => ({})), - removeLayer: jest.fn((_state, _layerId) => {}), - removeColumn: jest.fn((props) => {}), - getLayers: jest.fn((_state) => []), - uniqueLabels: jest.fn((_state) => ({})), - renderDimensionTrigger: jest.fn(), - renderDimensionEditor: jest.fn(), - getDropProps: jest.fn(), - onDrop: jest.fn(), - - // this is an additional property which doesn't exist on real datasources - // but can be used to validate whether specific API mock functions are called - publicAPIMock, - getErrorMessages: jest.fn((_state) => undefined), - checkIntegrity: jest.fn((_state) => []), - isTimeBased: jest.fn(), - isValidColumn: jest.fn(), - }; -} - -export const mockDatasource: DatasourceMock = createMockDatasource('testDatasource'); -export const mockDatasource2: DatasourceMock = createMockDatasource('testDatasource2'); - -export const datasourceMap = { - testDatasource2: mockDatasource2, - testDatasource: mockDatasource, -}; - -export function createExpressionRendererMock(): jest.Mock< - React.ReactElement, - [ReactExpressionRendererProps] -> { - return jest.fn((_) => ); -} - -export type FrameMock = jest.Mocked; -export function createMockFramePublicAPI(): FrameMock { - return { - datasourceLayers: {}, - }; -} - -export type FrameDatasourceMock = jest.Mocked; -export function createMockFrameDatasourceAPI(): FrameDatasourceMock { - return { - datasourceLayers: {}, - dateRange: { fromDate: 'now-7d', toDate: 'now' }, - query: { query: '', language: 'lucene' }, - filters: [], - }; -} - -export type Start = jest.Mocked; - -const createStartContract = (): Start => { - const startContract: Start = { - EmbeddableComponent: jest.fn(() => { - return Lens Embeddable Component; - }), - SaveModalComponent: jest.fn(() => { - return Lens Save Modal Component; - }), - canUseEditor: jest.fn(() => true), - navigateToPrefilledEditor: jest.fn(), - getXyVisTypes: jest.fn().mockReturnValue(new Promise((resolve) => resolve(visualizationTypes))), - }; - return startContract; -}; - -export const lensPluginMock = { - createStartContract, -}; - -export const defaultDoc = { - savedObjectId: '1234', - title: 'An extremely cool default document!', - expression: 'definitely a valid expression', - visualizationType: 'testVis', - state: { - query: 'kuery', - filters: [{ query: { match_phrase: { src: 'test' } } }], - datasourceStates: { - testDatasource: 'datasource', - }, - visualization: {}, - }, - references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], -} as unknown as Document; - -export function createMockTimefilter() { - const unsubscribe = jest.fn(); - - let timeFilter = { from: 'now-7d', to: 'now' }; - let subscriber: () => void; - return { - getTime: jest.fn(() => timeFilter), - setTime: jest.fn((newTimeFilter) => { - timeFilter = newTimeFilter; - if (subscriber) { - subscriber(); - } - }), - getTimeUpdate$: () => ({ - subscribe: ({ next }: { next: () => void }) => { - subscriber = next; - return unsubscribe; - }, - }), - calculateBounds: jest.fn(() => ({ - min: moment('2021-01-10T04:00:00.000Z'), - max: moment('2021-01-10T08:00:00.000Z'), - })), - getBounds: jest.fn(() => timeFilter), - getRefreshInterval: () => {}, - getRefreshIntervalDefaults: () => {}, - getAutoRefreshFetch$: () => new Observable(), - }; -} - -export const exactMatchDoc = { - ...defaultDoc, - sharingSavedObjectProps: { - outcome: 'exactMatch', - }, -}; - -export const mockStoreDeps = (deps?: { - lensServices?: LensAppServices; - datasourceMap?: DatasourceMap; - visualizationMap?: VisualizationMap; -}) => { - return { - datasourceMap: deps?.datasourceMap || datasourceMap, - visualizationMap: deps?.visualizationMap || visualizationMap, - lensServices: deps?.lensServices || makeDefaultServices(), - }; -}; - -export function mockDataPlugin( - sessionIdSubject = new Subject(), - initialSessionId?: string -) { - function createMockSearchService() { - let sessionIdCounter = initialSessionId ? 1 : 0; - let currentSessionId: string | undefined = initialSessionId; - const start = () => { - currentSessionId = `sessionId-${++sessionIdCounter}`; - return currentSessionId; - }; - return { - session: { - start: jest.fn(start), - clear: jest.fn(), - getSessionId: jest.fn(() => currentSessionId), - getSession$: jest.fn(() => sessionIdSubject.asObservable()), - }, - }; - } - - function createMockFilterManager() { - const unsubscribe = jest.fn(); - - let subscriber: () => void; - let filters: unknown = []; - - return { - getUpdates$: () => ({ - subscribe: ({ next }: { next: () => void }) => { - subscriber = next; - return unsubscribe; - }, - }), - setFilters: jest.fn((newFilters: unknown[]) => { - filters = newFilters; - if (subscriber) subscriber(); - }), - setAppFilters: jest.fn((newFilters: unknown[]) => { - filters = newFilters; - if (subscriber) subscriber(); - }), - getFilters: () => filters, - getGlobalFilters: () => { - // @ts-ignore - return filters.filter(esFilters.isFilterPinned); - }, - removeAll: () => { - filters = []; - subscriber(); - }, - }; - } - function createMockQueryString() { - return { - getQuery: jest.fn(() => ({ query: '', language: 'lucene' })), - setQuery: jest.fn(), - getDefaultQuery: jest.fn(() => ({ query: '', language: 'lucene' })), - }; - } - return { - query: { - filterManager: createMockFilterManager(), - timefilter: { - timefilter: createMockTimefilter(), - }, - queryString: createMockQueryString(), - state$: new Observable(), - }, - indexPatterns: { - get: jest.fn().mockImplementation((id) => Promise.resolve({ id, isTimeBased: () => true })), - }, - search: createMockSearchService(), - nowProvider: { - get: jest.fn(), - }, - fieldFormats: { - deserialize: jest.fn(), - }, - } as unknown as DataPublicPluginStart; -} - -export function makeDefaultServices( - sessionIdSubject = new Subject(), - sessionId: string | undefined = undefined, - doc = defaultDoc -): jest.Mocked { - const core = coreMock.createStart({ basePath: '/testbasepath' }); - core.uiSettings.get.mockImplementation( - jest.fn((type) => { - if (type === UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS) { - return { from: 'now-7d', to: 'now' }; - } else if (type === UI_SETTINGS.SEARCH_QUERY_LANGUAGE) { - return 'kuery'; - } else if (type === 'state:storeInSessionStorage') { - return false; - } else { - return []; - } - }) - ); - - const navigationStartMock = navigationPluginMock.createStartContract(); - - jest.spyOn(navigationStartMock.ui.TopNavMenu.prototype, 'constructor').mockImplementation(() => { - return
; - }); - - function makeAttributeService(): LensAttributeService { - const attributeServiceMock = mockAttributeService< - ResolvedLensSavedObjectAttributes, - LensByValueInput, - LensByReferenceInput - >( - DOC_TYPE, - { - saveMethod: jest.fn(), - unwrapMethod: jest.fn(), - checkForDuplicateTitle: jest.fn(), - }, - core - ); - attributeServiceMock.unwrapAttributes = jest.fn().mockResolvedValue(exactMatchDoc); - attributeServiceMock.wrapAttributes = jest.fn().mockResolvedValue({ - savedObjectId: (doc as unknown as LensByReferenceInput).savedObjectId, - }); - - return attributeServiceMock; - } - - return { - http: core.http, - chrome: core.chrome, - overlays: core.overlays, - uiSettings: core.uiSettings, - navigation: navigationStartMock, - notifications: core.notifications, - attributeService: makeAttributeService(), - inspector: { - adapters: getLensInspectorService(inspectorPluginMock.createStartContract()).adapters, - inspect: jest.fn(), - close: jest.fn(), - }, - dashboard: dashboardPluginMock.createStartContract(), - presentationUtil: presentationUtilPluginMock.createStartContract(core), - savedObjectsClient: core.savedObjects.client, - dashboardFeatureFlag: { allowByValueEmbeddables: false }, - stateTransfer: createEmbeddableStateTransferMock() as EmbeddableStateTransfer, - getOriginatingAppName: jest.fn(() => 'defaultOriginatingApp'), - application: { - ...core.application, - capabilities: { - ...core.application.capabilities, - visualize: { save: true, saveQuery: true, show: true }, - }, - getUrlForApp: jest.fn((appId: string) => `/testbasepath/app/${appId}#/`), - }, - data: mockDataPlugin(sessionIdSubject, sessionId), - fieldFormats: fieldFormatsServiceMock.createStartContract(), - storage: { - get: jest.fn(), - set: jest.fn(), - remove: jest.fn(), - clear: jest.fn(), - }, - spaces: spacesPluginMock.createStartContract(), - }; -} - -export const defaultState = { - searchSessionId: 'sessionId-1', - filters: [], - query: { language: 'lucene', query: '' }, - resolvedDateRange: { fromDate: '2021-01-10T04:00:00.000Z', toDate: '2021-01-10T08:00:00.000Z' }, - isFullscreenDatasource: false, - isSaveable: false, - isLoading: false, - isLinkedToOriginatingApp: false, - activeDatasourceId: 'testDatasource', - visualization: { - state: {}, - activeId: 'testVis', - }, - datasourceStates: mockDatasourceStates(), -}; - -export function makeLensStore({ - preloadedState, - dispatch, - storeDeps = mockStoreDeps(), -}: { - storeDeps?: LensStoreDeps; - preloadedState?: Partial; - dispatch?: jest.Mock; -}) { - const data = storeDeps.lensServices.data; - const store = makeConfigureStore(storeDeps, { - lens: { - ...defaultState, - query: data.query.queryString.getQuery(), - filters: data.query.filterManager.getGlobalFilters(), - resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter), - ...preloadedState, - }, - } as PreloadedState); - - const origDispatch = store.dispatch; - store.dispatch = jest.fn(dispatch || origDispatch); - return { store, deps: storeDeps }; -} - -export const mountWithProvider = async ( - component: React.ReactElement, - store?: { - storeDeps?: LensStoreDeps; - preloadedState?: Partial; - dispatch?: jest.Mock; - }, - options?: { - wrappingComponent?: React.FC<{ - children: React.ReactNode; - }>; - attachTo?: HTMLElement; - } -) => { - const { store: lensStore, deps } = makeLensStore(store || {}); - - let wrappingComponent: React.FC<{ - children: React.ReactNode; - }> = ({ children }) => {children}; - - let restOptions: { - attachTo?: HTMLElement | undefined; - }; - if (options) { - const { wrappingComponent: _wrappingComponent, ...rest } = options; - restOptions = rest; - - if (_wrappingComponent) { - wrappingComponent = ({ children }) => { - return _wrappingComponent({ - children: {children}, - }); - }; - } - } - - let instance: ReactWrapper = {} as ReactWrapper; - - await act(async () => { - instance = mount(component, { - wrappingComponent, - ...restOptions, - } as unknown as ReactWrapper); - }); - return { instance, lensStore, deps }; -}; diff --git a/x-pack/plugins/lens/public/mocks/data_plugin_mock.ts b/x-pack/plugins/lens/public/mocks/data_plugin_mock.ts new file mode 100644 index 0000000000000..daab2566b28fe --- /dev/null +++ b/x-pack/plugins/lens/public/mocks/data_plugin_mock.ts @@ -0,0 +1,122 @@ +/* + * 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 { Observable, Subject } from 'rxjs'; +import moment from 'moment'; +import { DataPublicPluginStart, esFilters } from '../../../../../src/plugins/data/public'; + +function createMockTimefilter() { + const unsubscribe = jest.fn(); + + let timeFilter = { from: 'now-7d', to: 'now' }; + let subscriber: () => void; + return { + getTime: jest.fn(() => timeFilter), + setTime: jest.fn((newTimeFilter) => { + timeFilter = newTimeFilter; + if (subscriber) { + subscriber(); + } + }), + getTimeUpdate$: () => ({ + subscribe: ({ next }: { next: () => void }) => { + subscriber = next; + return unsubscribe; + }, + }), + calculateBounds: jest.fn(() => ({ + min: moment('2021-01-10T04:00:00.000Z'), + max: moment('2021-01-10T08:00:00.000Z'), + })), + getBounds: jest.fn(() => timeFilter), + getRefreshInterval: () => {}, + getRefreshIntervalDefaults: () => {}, + getAutoRefreshFetch$: () => new Observable(), + }; +} + +export function mockDataPlugin( + sessionIdSubject = new Subject(), + initialSessionId?: string +) { + function createMockSearchService() { + let sessionIdCounter = initialSessionId ? 1 : 0; + let currentSessionId: string | undefined = initialSessionId; + const start = () => { + currentSessionId = `sessionId-${++sessionIdCounter}`; + return currentSessionId; + }; + return { + session: { + start: jest.fn(start), + clear: jest.fn(), + getSessionId: jest.fn(() => currentSessionId), + getSession$: jest.fn(() => sessionIdSubject.asObservable()), + }, + }; + } + + function createMockFilterManager() { + const unsubscribe = jest.fn(); + + let subscriber: () => void; + let filters: unknown = []; + + return { + getUpdates$: () => ({ + subscribe: ({ next }: { next: () => void }) => { + subscriber = next; + return unsubscribe; + }, + }), + setFilters: jest.fn((newFilters: unknown[]) => { + filters = newFilters; + if (subscriber) subscriber(); + }), + setAppFilters: jest.fn((newFilters: unknown[]) => { + filters = newFilters; + if (subscriber) subscriber(); + }), + getFilters: () => filters, + getGlobalFilters: () => { + // @ts-ignore + return filters.filter(esFilters.isFilterPinned); + }, + removeAll: () => { + filters = []; + subscriber(); + }, + }; + } + function createMockQueryString() { + return { + getQuery: jest.fn(() => ({ query: '', language: 'lucene' })), + setQuery: jest.fn(), + getDefaultQuery: jest.fn(() => ({ query: '', language: 'lucene' })), + }; + } + return { + query: { + filterManager: createMockFilterManager(), + timefilter: { + timefilter: createMockTimefilter(), + }, + queryString: createMockQueryString(), + state$: new Observable(), + }, + indexPatterns: { + get: jest.fn().mockImplementation((id) => Promise.resolve({ id, isTimeBased: () => true })), + }, + search: createMockSearchService(), + nowProvider: { + get: jest.fn(), + }, + fieldFormats: { + deserialize: jest.fn(), + }, + } as unknown as DataPublicPluginStart; +} diff --git a/x-pack/plugins/lens/public/mocks/datasource_mock.ts b/x-pack/plugins/lens/public/mocks/datasource_mock.ts new file mode 100644 index 0000000000000..2614b1d5fdc94 --- /dev/null +++ b/x-pack/plugins/lens/public/mocks/datasource_mock.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DatasourcePublicAPI, Datasource } from '../types'; + +export type DatasourceMock = jest.Mocked & { + publicAPIMock: jest.Mocked; +}; + +export function createMockDatasource(id: string): DatasourceMock { + const publicAPIMock: jest.Mocked = { + datasourceId: id, + getTableSpec: jest.fn(() => []), + getOperationForColumnId: jest.fn(), + }; + + return { + id: 'testDatasource', + clearLayer: jest.fn((state, _layerId) => state), + getDatasourceSuggestionsForField: jest.fn((_state, _item, filterFn) => []), + getDatasourceSuggestionsForVisualizeField: jest.fn((_state, _indexpatternId, _fieldName) => []), + getDatasourceSuggestionsFromCurrentState: jest.fn((_state) => []), + getPersistableState: jest.fn((x) => ({ + state: x, + savedObjectReferences: [{ type: 'index-pattern', id: 'mockip', name: 'mockip' }], + })), + getPublicAPI: jest.fn().mockReturnValue(publicAPIMock), + initialize: jest.fn((_state?) => Promise.resolve()), + renderDataPanel: jest.fn(), + renderLayerPanel: jest.fn(), + toExpression: jest.fn((_frame, _state) => null), + insertLayer: jest.fn((_state, _newLayerId) => ({})), + removeLayer: jest.fn((_state, _layerId) => {}), + removeColumn: jest.fn((props) => {}), + getLayers: jest.fn((_state) => []), + uniqueLabels: jest.fn((_state) => ({})), + renderDimensionTrigger: jest.fn(), + renderDimensionEditor: jest.fn(), + getDropProps: jest.fn(), + onDrop: jest.fn(), + + // this is an additional property which doesn't exist on real datasources + // but can be used to validate whether specific API mock functions are called + publicAPIMock, + getErrorMessages: jest.fn((_state) => undefined), + checkIntegrity: jest.fn((_state) => []), + isTimeBased: jest.fn(), + isValidColumn: jest.fn(), + }; +} + +export function mockDatasourceMap() { + const datasource = createMockDatasource('testDatasource'); + datasource.getDatasourceSuggestionsFromCurrentState.mockReturnValue([ + { + state: {}, + table: { + columns: [], + isMultiRow: true, + layerId: 'a', + changeType: 'unchanged', + }, + keptLayerIds: ['a'], + }, + ]); + + datasource.getLayers.mockReturnValue(['a']); + return { + testDatasource2: createMockDatasource('testDatasource2'), + testDatasource: datasource, + }; +} + +export const datasourceMap = mockDatasourceMap(); diff --git a/x-pack/plugins/lens/public/mocks/expression_renderer_mock.tsx b/x-pack/plugins/lens/public/mocks/expression_renderer_mock.tsx new file mode 100644 index 0000000000000..644021e8a69c2 --- /dev/null +++ b/x-pack/plugins/lens/public/mocks/expression_renderer_mock.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { ReactExpressionRendererProps } from 'src/plugins/expressions/public'; + +export function createExpressionRendererMock(): jest.Mock< + React.ReactElement, + [ReactExpressionRendererProps] +> { + return jest.fn((_) => ); +} diff --git a/x-pack/plugins/lens/public/mocks/index.ts b/x-pack/plugins/lens/public/mocks/index.ts new file mode 100644 index 0000000000000..2dd32a1679f1b --- /dev/null +++ b/x-pack/plugins/lens/public/mocks/index.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FramePublicAPI, FrameDatasourceAPI } from '../types'; +export { mockDataPlugin } from './data_plugin_mock'; +export { + visualizationMap, + createMockVisualization, + mockVisualizationMap, +} from './visualization_mock'; +export { datasourceMap, mockDatasourceMap, createMockDatasource } from './datasource_mock'; +export type { DatasourceMock } from './datasource_mock'; +export { createExpressionRendererMock } from './expression_renderer_mock'; +export { defaultDoc, exactMatchDoc, makeDefaultServices } from './services_mock'; +export { + mockStoreDeps, + mockDatasourceStates, + defaultState, + makeLensStore, + MountStoreProps, + mountWithProvider, +} from './store_mocks'; +export { lensPluginMock } from './lens_plugin_mock'; + +export type FrameMock = jest.Mocked; + +export const createMockFramePublicAPI = (): FrameMock => ({ + datasourceLayers: {}, +}); + +export type FrameDatasourceMock = jest.Mocked; + +export const createMockFrameDatasourceAPI = (): FrameDatasourceMock => ({ + datasourceLayers: {}, + dateRange: { fromDate: 'now-7d', toDate: 'now' }, + query: { query: '', language: 'lucene' }, + filters: [], +}); diff --git a/x-pack/plugins/lens/public/mocks/lens_plugin_mock.tsx b/x-pack/plugins/lens/public/mocks/lens_plugin_mock.tsx new file mode 100644 index 0000000000000..a92533a89ba67 --- /dev/null +++ b/x-pack/plugins/lens/public/mocks/lens_plugin_mock.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 React from 'react'; +import { LensPublicStart } from '..'; +import { visualizationTypes } from '../xy_visualization/types'; + +type Start = jest.Mocked; + +export const lensPluginMock = { + createStartContract: (): Start => { + const startContract: Start = { + EmbeddableComponent: jest.fn(() => { + return Lens Embeddable Component; + }), + SaveModalComponent: jest.fn(() => { + return Lens Save Modal Component; + }), + canUseEditor: jest.fn(() => true), + navigateToPrefilledEditor: jest.fn(), + getXyVisTypes: jest + .fn() + .mockReturnValue(new Promise((resolve) => resolve(visualizationTypes))), + }; + return startContract; + }, +}; diff --git a/x-pack/plugins/lens/public/mocks/services_mock.tsx b/x-pack/plugins/lens/public/mocks/services_mock.tsx new file mode 100644 index 0000000000000..c6db0dfb6aae8 --- /dev/null +++ b/x-pack/plugins/lens/public/mocks/services_mock.tsx @@ -0,0 +1,143 @@ +/* + * 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 { Subject } from 'rxjs'; +import { coreMock } from 'src/core/public/mocks'; +import { navigationPluginMock } from '../../../../../src/plugins/navigation/public/mocks'; +import { LensAppServices } from '../app_plugin/types'; +import { DOC_TYPE } from '../../common'; +import { UI_SETTINGS } from '../../../../../src/plugins/data/public'; +import { inspectorPluginMock } from '../../../../../src/plugins/inspector/public/mocks'; +import { spacesPluginMock } from '../../../spaces/public/mocks'; +import { dashboardPluginMock } from '../../../../../src/plugins/dashboard/public/mocks'; +import type { + LensByValueInput, + LensByReferenceInput, + ResolvedLensSavedObjectAttributes, +} from '../embeddable/embeddable'; +import { + mockAttributeService, + createEmbeddableStateTransferMock, +} from '../../../../../src/plugins/embeddable/public/mocks'; +import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; +import type { LensAttributeService } from '../lens_attribute_service'; +import type { EmbeddableStateTransfer } from '../../../../../src/plugins/embeddable/public'; + +import { presentationUtilPluginMock } from '../../../../../src/plugins/presentation_util/public/mocks'; +import { mockDataPlugin } from './data_plugin_mock'; +import { getLensInspectorService } from '../lens_inspector_service'; + +export const defaultDoc = { + savedObjectId: '1234', + title: 'An extremely cool default document!', + expression: 'definitely a valid expression', + visualizationType: 'testVis', + state: { + query: 'kuery', + filters: [{ query: { match_phrase: { src: 'test' } } }], + datasourceStates: { + testDatasource: 'datasource', + }, + visualization: {}, + }, + references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], +} as unknown as Document; + +export const exactMatchDoc = { + ...defaultDoc, + sharingSavedObjectProps: { + outcome: 'exactMatch', + }, +}; + +export function makeDefaultServices( + sessionIdSubject = new Subject(), + sessionId: string | undefined = undefined, + doc = defaultDoc +): jest.Mocked { + const core = coreMock.createStart({ basePath: '/testbasepath' }); + core.uiSettings.get.mockImplementation( + jest.fn((type) => { + if (type === UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS) { + return { from: 'now-7d', to: 'now' }; + } else if (type === UI_SETTINGS.SEARCH_QUERY_LANGUAGE) { + return 'kuery'; + } else if (type === 'state:storeInSessionStorage') { + return false; + } else { + return []; + } + }) + ); + + const navigationStartMock = navigationPluginMock.createStartContract(); + + jest.spyOn(navigationStartMock.ui.TopNavMenu.prototype, 'constructor').mockImplementation(() => { + return
; + }); + + function makeAttributeService(): LensAttributeService { + const attributeServiceMock = mockAttributeService< + ResolvedLensSavedObjectAttributes, + LensByValueInput, + LensByReferenceInput + >( + DOC_TYPE, + { + saveMethod: jest.fn(), + unwrapMethod: jest.fn(), + checkForDuplicateTitle: jest.fn(), + }, + core + ); + attributeServiceMock.unwrapAttributes = jest.fn().mockResolvedValue(exactMatchDoc); + attributeServiceMock.wrapAttributes = jest.fn().mockResolvedValue({ + savedObjectId: (doc as unknown as LensByReferenceInput).savedObjectId, + }); + + return attributeServiceMock; + } + + return { + http: core.http, + chrome: core.chrome, + overlays: core.overlays, + uiSettings: core.uiSettings, + navigation: navigationStartMock, + notifications: core.notifications, + attributeService: makeAttributeService(), + inspector: { + adapters: getLensInspectorService(inspectorPluginMock.createStartContract()).adapters, + inspect: jest.fn(), + close: jest.fn(), + }, + dashboard: dashboardPluginMock.createStartContract(), + presentationUtil: presentationUtilPluginMock.createStartContract(core), + savedObjectsClient: core.savedObjects.client, + dashboardFeatureFlag: { allowByValueEmbeddables: false }, + stateTransfer: createEmbeddableStateTransferMock() as EmbeddableStateTransfer, + getOriginatingAppName: jest.fn(() => 'defaultOriginatingApp'), + application: { + ...core.application, + capabilities: { + ...core.application.capabilities, + visualize: { save: true, saveQuery: true, show: true }, + }, + getUrlForApp: jest.fn((appId: string) => `/testbasepath/app/${appId}#/`), + }, + data: mockDataPlugin(sessionIdSubject, sessionId), + fieldFormats: fieldFormatsServiceMock.createStartContract(), + storage: { + get: jest.fn(), + set: jest.fn(), + remove: jest.fn(), + clear: jest.fn(), + }, + spaces: spacesPluginMock.createStartContract(), + }; +} diff --git a/x-pack/plugins/lens/public/mocks/store_mocks.tsx b/x-pack/plugins/lens/public/mocks/store_mocks.tsx new file mode 100644 index 0000000000000..1b1d83ef2892d --- /dev/null +++ b/x-pack/plugins/lens/public/mocks/store_mocks.tsx @@ -0,0 +1,140 @@ +/* + * 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'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { ReactWrapper } from 'enzyme'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { mountWithIntl as mount } from '@kbn/test/jest'; +import { Provider } from 'react-redux'; +import { act } from 'react-dom/test-utils'; +import { PreloadedState } from '@reduxjs/toolkit'; +import { LensAppServices } from '../app_plugin/types'; + +import { + makeConfigureStore, + LensAppState, + LensState, + LensStoreDeps, +} from '../state_management/index'; +import { getResolvedDateRange } from '../utils'; +import { DatasourceMap, VisualizationMap } from '../types'; +import { mockVisualizationMap } from './visualization_mock'; +import { mockDatasourceMap } from './datasource_mock'; +import { makeDefaultServices } from './services_mock'; + +export const mockStoreDeps = (deps?: { + lensServices?: LensAppServices; + datasourceMap?: DatasourceMap; + visualizationMap?: VisualizationMap; +}) => { + return { + datasourceMap: deps?.datasourceMap || mockDatasourceMap(), + visualizationMap: deps?.visualizationMap || mockVisualizationMap(), + lensServices: deps?.lensServices || makeDefaultServices(), + }; +}; + +export function mockDatasourceStates() { + return { + testDatasource: { + state: {}, + isLoading: false, + }, + }; +} + +export const defaultState = { + searchSessionId: 'sessionId-1', + filters: [], + query: { language: 'lucene', query: '' }, + resolvedDateRange: { fromDate: '2021-01-10T04:00:00.000Z', toDate: '2021-01-10T08:00:00.000Z' }, + isFullscreenDatasource: false, + isSaveable: false, + isLoading: false, + isLinkedToOriginatingApp: false, + activeDatasourceId: 'testDatasource', + visualization: { + state: {}, + activeId: 'testVis', + }, + datasourceStates: mockDatasourceStates(), +}; + +export function makeLensStore({ + preloadedState, + dispatch, + storeDeps = mockStoreDeps(), +}: { + storeDeps?: LensStoreDeps; + preloadedState?: Partial; + dispatch?: jest.Mock; +}) { + const data = storeDeps.lensServices.data; + const store = makeConfigureStore(storeDeps, { + lens: { + ...defaultState, + query: data.query.queryString.getQuery(), + filters: data.query.filterManager.getGlobalFilters(), + resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter), + ...preloadedState, + }, + } as PreloadedState); + + const origDispatch = store.dispatch; + store.dispatch = jest.fn(dispatch || origDispatch); + return { store, deps: storeDeps }; +} + +export interface MountStoreProps { + storeDeps?: LensStoreDeps; + preloadedState?: Partial; + dispatch?: jest.Mock; +} + +export const mountWithProvider = async ( + component: React.ReactElement, + store?: MountStoreProps, + options?: { + wrappingComponent?: React.FC<{ + children: React.ReactNode; + }>; + attachTo?: HTMLElement; + } +) => { + const { store: lensStore, deps } = makeLensStore(store || {}); + + let wrappingComponent: React.FC<{ + children: React.ReactNode; + }> = ({ children }) => {children}; + + let restOptions: { + attachTo?: HTMLElement | undefined; + }; + if (options) { + const { wrappingComponent: _wrappingComponent, ...rest } = options; + restOptions = rest; + + if (_wrappingComponent) { + wrappingComponent = ({ children }) => { + return _wrappingComponent({ + children: {children}, + }); + }; + } + } + + let instance: ReactWrapper = {} as ReactWrapper; + + await act(async () => { + instance = mount(component, { + wrappingComponent, + ...restOptions, + } as unknown as ReactWrapper); + }); + return { instance, lensStore, deps }; +}; diff --git a/x-pack/plugins/lens/public/mocks/visualization_mock.ts b/x-pack/plugins/lens/public/mocks/visualization_mock.ts new file mode 100644 index 0000000000000..199bf9a9db77a --- /dev/null +++ b/x-pack/plugins/lens/public/mocks/visualization_mock.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { layerTypes } from '../../common'; +import { Visualization, VisualizationMap } from '../types'; + +export function createMockVisualization(id = 'testVis'): jest.Mocked { + return { + id, + clearLayer: jest.fn((state, _layerId) => state), + removeLayer: jest.fn(), + getLayerIds: jest.fn((_state) => ['layer1']), + getSupportedLayers: jest.fn(() => [{ type: layerTypes.DATA, label: 'Data Layer' }]), + getLayerType: jest.fn((_state, _layerId) => layerTypes.DATA), + visualizationTypes: [ + { + icon: 'empty', + id, + label: 'TEST', + groupLabel: `${id}Group`, + }, + ], + appendLayer: jest.fn(), + getVisualizationTypeId: jest.fn((_state) => 'empty'), + getDescription: jest.fn((_state) => ({ label: '' })), + switchVisualizationType: jest.fn((_, x) => x), + getSuggestions: jest.fn((_options) => []), + initialize: jest.fn((_frame, _state?) => ({ newState: 'newState' })), + getConfiguration: jest.fn((props) => ({ + groups: [ + { + groupId: 'a', + groupLabel: 'a', + layerId: 'layer1', + supportsMoreColumns: true, + accessors: [], + filterOperations: jest.fn(() => true), + dataTestSubj: 'mockVisA', + }, + ], + })), + toExpression: jest.fn((_state, _frame) => null), + toPreviewExpression: jest.fn((_state, _frame) => null), + + setDimension: jest.fn(), + removeDimension: jest.fn(), + getErrorMessages: jest.fn((_state) => undefined), + renderDimensionEditor: jest.fn(), + }; +} + +export const mockVisualizationMap = (): VisualizationMap => { + return { + testVis: createMockVisualization(), + testVis2: createMockVisualization(), + }; +}; + +export const visualizationMap = mockVisualizationMap(); From 233dac32b404997115a60f98665367d23b4dc584 Mon Sep 17 00:00:00 2001 From: Dave Snider Date: Thu, 21 Oct 2021 17:26:49 -0400 Subject: [PATCH 33/40] Copy changes for sample data header (#115963) --- .../public/application/components/tutorial_directory.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/plugins/home/public/application/components/tutorial_directory.js b/src/plugins/home/public/application/components/tutorial_directory.js index ac0d1524145a1..a1a93e3eba542 100644 --- a/src/plugins/home/public/application/components/tutorial_directory.js +++ b/src/plugins/home/public/application/components/tutorial_directory.js @@ -218,7 +218,13 @@ class TutorialDirectoryUi extends React.Component { pageTitle: ( + ), + description: ( + ), tabs, From 41302211236ee9871bdbe78536776911d113ba07 Mon Sep 17 00:00:00 2001 From: Poff Poffenberger Date: Thu, 21 Oct 2021 18:57:16 -0500 Subject: [PATCH 34/40] [Controls] Data view and field pickers (#116018) Added file picker and field picker to presentationUtil --- src/plugins/presentation_util/kibana.json | 2 +- .../data_view_picker.stories.tsx | 94 +++++++++++ .../data_view_picker/data_view_picker.tsx | 114 +++++++++++++ .../components/field_picker/field_picker.scss | 15 ++ .../field_picker/field_picker.stories.tsx | 89 ++++++++++ .../components/field_picker/field_picker.tsx | 152 ++++++++++++++++++ .../components/field_picker/field_search.tsx | 125 ++++++++++++++ 7 files changed, 590 insertions(+), 1 deletion(-) create mode 100644 src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.stories.tsx create mode 100644 src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx create mode 100644 src/plugins/presentation_util/public/components/field_picker/field_picker.scss create mode 100644 src/plugins/presentation_util/public/components/field_picker/field_picker.stories.tsx create mode 100644 src/plugins/presentation_util/public/components/field_picker/field_picker.tsx create mode 100644 src/plugins/presentation_util/public/components/field_picker/field_search.tsx diff --git a/src/plugins/presentation_util/kibana.json b/src/plugins/presentation_util/kibana.json index d7fe9b558e606..71ac224d1976a 100644 --- a/src/plugins/presentation_util/kibana.json +++ b/src/plugins/presentation_util/kibana.json @@ -10,6 +10,6 @@ "server": true, "ui": true, "extraPublicDirs": ["common/lib"], - "requiredPlugins": ["savedObjects"], + "requiredPlugins": ["savedObjects", "kibanaReact"], "optionalPlugins": [] } diff --git a/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.stories.tsx b/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.stories.tsx new file mode 100644 index 0000000000000..1a29d0536a290 --- /dev/null +++ b/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.stories.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; + +import { DataView, DataViewField, IIndexPatternFieldList } from '../../../../data_views/common'; + +import { StorybookParams } from '../../services/storybook'; +import { DataViewPicker } from './data_view_picker'; + +// TODO: we probably should remove this once the PR is merged that has better data views for stories +const flightFieldNames: string[] = [ + 'AvgTicketPrice', + 'Cancelled', + 'Carrier', + 'dayOfWeek', + 'Dest', + 'DestAirportID', + 'DestCityName', + 'DestCountry', + 'DestLocation', + 'DestRegion', + 'DestWeather', + 'DistanceKilometers', + 'DistanceMiles', + 'FlightDelay', + 'FlightDelayMin', + 'FlightDelayType', + 'FlightNum', + 'FlightTimeHour', + 'FlightTimeMin', + 'Origin', + 'OriginAirportID', + 'OriginCityName', + 'OriginCountry', + 'OriginLocation', + 'OriginRegion', + 'OriginWeather', + 'timestamp', +]; +const flightFieldByName: { [key: string]: DataViewField } = {}; +flightFieldNames.forEach( + (flightFieldName) => + (flightFieldByName[flightFieldName] = { + name: flightFieldName, + type: 'string', + } as unknown as DataViewField) +); + +// Change some types manually for now +flightFieldByName.Cancelled = { name: 'Cancelled', type: 'boolean' } as DataViewField; +flightFieldByName.timestamp = { name: 'timestamp', type: 'date' } as DataViewField; + +const flightFields: DataViewField[] = Object.values(flightFieldByName); +const storybookFlightsDataView: DataView = { + id: 'demoDataFlights', + title: 'demo data flights', + fields: flightFields as unknown as IIndexPatternFieldList, + getFieldByName: (name: string) => flightFieldByName[name], +} as unknown as DataView; + +export default { + component: DataViewPicker, + title: 'Data View Picker', + argTypes: {}, +}; + +export function Example({}: {} & StorybookParams) { + const dataViews = [storybookFlightsDataView]; + + const [dataView, setDataView] = useState(undefined); + + const onChange = (newId: string) => { + const newIndexPattern = dataViews.find((ip) => ip.id === newId); + + setDataView(newIndexPattern); + }; + + const triggerLabel = dataView?.title || 'Choose Data View'; + + return ( + + ); +} diff --git a/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx b/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx new file mode 100644 index 0000000000000..38ec4f16f9432 --- /dev/null +++ b/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 React, { useState } from 'react'; +import { EuiPopover, EuiPopoverTitle, EuiSelectable, EuiSelectableProps } from '@elastic/eui'; +import { DataView } from '../../../../data_views/common'; + +import { ToolbarButton, ToolbarButtonProps } from '../../../../kibana_react/public'; + +export type DataViewTriggerProps = ToolbarButtonProps & { + label: string; + title?: string; +}; + +export function DataViewPicker({ + dataViews, + selectedDataViewId, + onChangeIndexPattern, + trigger, + selectableProps, +}: { + dataViews: DataView[]; + selectedDataViewId?: string; + trigger: DataViewTriggerProps; + onChangeIndexPattern: (newId: string) => void; + selectableProps?: EuiSelectableProps; +}) { + const [isPopoverOpen, setPopoverIsOpen] = useState(false); + + const isMissingCurrent = !dataViews.some(({ id }) => id === selectedDataViewId); + + // be careful to only add color with a value, otherwise it will fallbacks to "primary" + const colorProp = isMissingCurrent + ? { + color: 'danger' as const, + } + : {}; + + const createTrigger = function () { + const { label, title, ...rest } = trigger; + return ( + setPopoverIsOpen(!isPopoverOpen)} + fullWidth + {...colorProp} + {...rest} + > + {label} + + ); + }; + + return ( + <> + setPopoverIsOpen(false)} + display="block" + panelPaddingSize="s" + ownFocus + > +
+ + {i18n.translate('presentationUtil.dataViewPicker.changeDataViewTitle', { + defaultMessage: 'Data view', + })} + + + {...selectableProps} + searchable + singleSelection="always" + options={dataViews.map(({ title, id }) => ({ + key: id, + label: title, + value: id, + checked: id === selectedDataViewId ? 'on' : undefined, + }))} + onChange={(choices) => { + const choice = choices.find(({ checked }) => checked) as unknown as { + value: string; + }; + onChangeIndexPattern(choice.value); + setPopoverIsOpen(false); + }} + searchProps={{ + compressed: true, + ...(selectableProps ? selectableProps.searchProps : undefined), + }} + > + {(list, search) => ( + <> + {search} + {list} + + )} + +
+
+ + ); +} diff --git a/src/plugins/presentation_util/public/components/field_picker/field_picker.scss b/src/plugins/presentation_util/public/components/field_picker/field_picker.scss new file mode 100644 index 0000000000000..c07cf99ed03d6 --- /dev/null +++ b/src/plugins/presentation_util/public/components/field_picker/field_picker.scss @@ -0,0 +1,15 @@ +.presFieldPicker__fieldButton { + box-shadow: 0 .8px .8px rgba(0,0,0,.04),0 2.3px 2px rgba(0,0,0,.03); + background: #FFF; + border: 1px dashed transparent; +} + +.presFieldPicker__fieldPanel { + height: 300px; + overflow-y: scroll; +} + +.presFieldPicker__container--disabled { + opacity: .7; + pointer-events: none; +} \ No newline at end of file diff --git a/src/plugins/presentation_util/public/components/field_picker/field_picker.stories.tsx b/src/plugins/presentation_util/public/components/field_picker/field_picker.stories.tsx new file mode 100644 index 0000000000000..c5654254ea70a --- /dev/null +++ b/src/plugins/presentation_util/public/components/field_picker/field_picker.stories.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +import { FieldPicker } from './field_picker'; + +import { DataView, DataViewField, IIndexPatternFieldList } from '../../../../data_views/common'; + +// TODO: we probably should remove this once the PR is merged that has better data views for stories +const flightFieldNames: string[] = [ + 'AvgTicketPrice', + 'Cancelled', + 'Carrier', + 'dayOfWeek', + 'Dest', + 'DestAirportID', + 'DestCityName', + 'DestCountry', + 'DestLocation', + 'DestRegion', + 'DestWeather', + 'DistanceKilometers', + 'DistanceMiles', + 'FlightDelay', + 'FlightDelayMin', + 'FlightDelayType', + 'FlightNum', + 'FlightTimeHour', + 'FlightTimeMin', + 'Origin', + 'OriginAirportID', + 'OriginCityName', + 'OriginCountry', + 'OriginLocation', + 'OriginRegion', + 'OriginWeather', + 'timestamp', +]; +const flightFieldByName: { [key: string]: DataViewField } = {}; +flightFieldNames.forEach( + (flightFieldName) => + (flightFieldByName[flightFieldName] = { + name: flightFieldName, + type: 'string', + } as unknown as DataViewField) +); + +// Change some types manually for now +flightFieldByName.Cancelled = { name: 'Cancelled', type: 'boolean' } as DataViewField; +flightFieldByName.timestamp = { name: 'timestamp', type: 'date' } as DataViewField; + +const flightFields: DataViewField[] = Object.values(flightFieldByName); +const storybookFlightsDataView: DataView = { + id: 'demoDataFlights', + title: 'demo data flights', + fields: flightFields as unknown as IIndexPatternFieldList, + getFieldByName: (name: string) => flightFieldByName[name], +} as unknown as DataView; + +export default { + component: FieldPicker, + title: 'Field Picker', +}; + +export const FieldPickerWithDataView = () => { + return ; +}; + +export const FieldPickerWithFilter = () => { + return ( + { + // Only show fields with "Dest" in the title + return f.name.includes('Dest'); + }} + /> + ); +}; + +export const FieldPickerWithoutIndexPattern = () => { + return ; +}; diff --git a/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx b/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx new file mode 100644 index 0000000000000..bbdf389ccee14 --- /dev/null +++ b/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import { sortBy, uniq } from 'lodash'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { DataView, DataViewField } from '../../../../data_views/common'; +import { FieldIcon, FieldButton } from '../../../../kibana_react/public'; + +import { FieldSearch } from './field_search'; + +import './field_picker.scss'; + +export interface Props { + dataView: DataView | null; + filterPredicate?: (f: DataViewField) => boolean; +} + +export const FieldPicker = ({ dataView, filterPredicate }: Props) => { + const [nameFilter, setNameFilter] = useState(''); + const [typesFilter, setTypesFilter] = useState([]); + const [selectedField, setSelectedField] = useState(null); + + // Retrieve, filter, and sort fields from data view + const fields = dataView + ? sortBy( + dataView.fields + .filter( + (f) => + f.name.includes(nameFilter) && + (typesFilter.length === 0 || typesFilter.includes(f.type as string)) + ) + .filter((f) => (filterPredicate ? filterPredicate(f) : true)), + ['name'] + ) + : []; + + const uniqueTypes = dataView ? uniq(dataView.fields.map((f) => f.type as string)) : []; + + return ( + + + setNameFilter(val)} + searchValue={nameFilter} + onFieldTypesChange={(types) => setTypesFilter(types)} + fieldTypesValue={typesFilter} + availableFieldTypes={uniqueTypes} + /> + + + + {fields.length > 0 && ( + + {fields.map((f, i) => { + return ( + + setSelectedField(f)} + isActive={f.name === selectedField?.name} + fieldName={f.name} + fieldIcon={} + /> + + ); + })} + + )} + {!dataView && ( + + + + + + + + )} + {dataView && fields.length === 0 && ( + + + + + + + + )} + + + {selectedField && ( + + +

+ +

+
+
+ + } + /> +
+
+ )} +
+ ); +}; diff --git a/src/plugins/presentation_util/public/components/field_picker/field_search.tsx b/src/plugins/presentation_util/public/components/field_picker/field_search.tsx new file mode 100644 index 0000000000000..d3c6c728b3d08 --- /dev/null +++ b/src/plugins/presentation_util/public/components/field_picker/field_search.tsx @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFieldSearch, + EuiFilterGroup, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiContextMenuPanel, + EuiContextMenuItem, + EuiOutsideClickDetector, + EuiFilterButton, + EuiSpacer, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { FieldIcon } from '../../../../kibana_react/public'; + +export interface Props { + onSearchChange: (value: string) => void; + searchValue?: string; + + onFieldTypesChange: (value: string[]) => void; + fieldTypesValue: string[]; + + availableFieldTypes: string[]; +} + +export function FieldSearch({ + onSearchChange, + searchValue, + onFieldTypesChange, + fieldTypesValue, + availableFieldTypes, +}: Props) { + const searchPlaceholder = i18n.translate('presentationUtil.fieldSearch.searchPlaceHolder', { + defaultMessage: 'Search field names', + }); + + const [isPopoverOpen, setPopoverOpen] = useState(false); + + const handleFilterButtonClicked = () => { + setPopoverOpen(!isPopoverOpen); + }; + + const buttonContent = ( + 0} + numFilters={0} + hasActiveFilters={fieldTypesValue.length > 0} + numActiveFilters={fieldTypesValue.length} + onClick={handleFilterButtonClicked} + > + + + ); + + return ( + + + + onSearchChange(event.currentTarget.value)} + placeholder={searchPlaceholder} + value={searchValue} + /> + + + + {}} isDisabled={!isPopoverOpen}> + + { + setPopoverOpen(false); + }} + button={buttonContent} + > + ( + { + if (fieldTypesValue.includes(type)) { + onFieldTypesChange(fieldTypesValue.filter((f) => f !== type)); + } else { + onFieldTypesChange([...fieldTypesValue, type]); + } + }} + > + + + {type} + + + ))} + /> + + + + + ); +} From a8e16ba9aa793a07074054a390e56d5d4b49051a Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 22 Oct 2021 02:14:03 +0100 Subject: [PATCH 35/40] chore(NA): upgrades lmdb-store to v1.6.11 (#115971) * chore(NA): upgrades lmdb-store to v1.6.10 * chore(NA): upgrade into v1.6.11 --- package.json | 2 +- yarn.lock | 23 +++++++++++------------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 3df7b65315a85..e6b17783197bc 100644 --- a/package.json +++ b/package.json @@ -743,7 +743,7 @@ "jsondiffpatch": "0.4.1", "license-checker": "^16.0.0", "listr": "^0.14.1", - "lmdb-store": "^1.6.8", + "lmdb-store": "^1.6.11", "marge": "^1.0.1", "micromatch": "3.1.10", "minimist": "^1.2.5", diff --git a/yarn.lock b/yarn.lock index 86c4c9801f56e..669f8321fb4fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19212,18 +19212,17 @@ listr@^0.14.1: p-map "^2.0.0" rxjs "^6.3.3" -lmdb-store@^1.6.8: - version "1.6.8" - resolved "https://registry.yarnpkg.com/lmdb-store/-/lmdb-store-1.6.8.tgz#f57c1fa4a8e8e7a73d58523d2bfbcee96782311f" - integrity sha512-Ltok13VVAfgO5Fdj/jVzXjPJZjefl1iENEHerZyAfAlzFUhvOrA73UdKItqmEPC338U29mm56ZBQr5NJQiKXow== +lmdb-store@^1.6.11: + version "1.6.11" + resolved "https://registry.yarnpkg.com/lmdb-store/-/lmdb-store-1.6.11.tgz#801da597af8c7a01c81f87d5cc7a7497e381236d" + integrity sha512-hIvoGmHGsFhb2VRCmfhodA/837ULtJBwRHSHKIzhMB7WtPH6BRLPsvXp1MwD3avqGzuZfMyZDUp3tccLvr721Q== dependencies: - mkdirp "^1.0.4" nan "^2.14.2" node-gyp-build "^4.2.3" ordered-binary "^1.0.0" weak-lru-cache "^1.0.0" optionalDependencies: - msgpackr "^1.3.7" + msgpackr "^1.4.7" load-bmfont@^1.3.1, load-bmfont@^1.4.0: version "1.4.0" @@ -20672,7 +20671,7 @@ ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -msgpackr-extract@^1.0.13: +msgpackr-extract@^1.0.14: version "1.0.14" resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-1.0.14.tgz#87d3fe825d226e7f3d9fe136375091137f958561" integrity sha512-t8neMf53jNZRF+f0H9VvEUVvtjGZ21odSBRmFfjZiyxr9lKYY0mpY3kSWZAIc7YWXtCZGOvDQVx2oqcgGiRBrw== @@ -20680,12 +20679,12 @@ msgpackr-extract@^1.0.13: nan "^2.14.2" node-gyp-build "^4.2.3" -msgpackr@^1.3.7: - version "1.4.2" - resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.4.2.tgz#52ddf0130ccdb1067957fe61c8be828e82bb29ce" - integrity sha512-6gvaU+3xIflium8eJcruT66kLQr14lgTEmXtDm7KKzBSWHljD7pqu3VBQv1PDipFD5UGXLTIxGg5hGbO/jTvxQ== +msgpackr@^1.4.7: + version "1.4.7" + resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.4.7.tgz#d802ade841e7d2e873000b491cdda6574a3d5748" + integrity sha512-bhC8Ed1au3L3oHaR/fe4lk4w7PLGFcWQ5XY/Tk9N6tzDRz8YndjCG68TD8zcvYZoxNtw767eF/7VpaTpU9kf9w== optionalDependencies: - msgpackr-extract "^1.0.13" + msgpackr-extract "^1.0.14" multicast-dns-service-types@^1.1.0: version "1.1.0" From 286eacc79b919a2e16744dd4e7b90b8002e281a5 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 21 Oct 2021 19:34:19 -0700 Subject: [PATCH 36/40] [Alerting] Telemetry fix for min/max number of actions a rule has associated with (#115496) * [Alerting] Telemetry for max number of actions a rule has and max alerts created on execution * - * Update rules_client.ts * fixed task data * added unit test * fixed by adding runtime field * fixed task data * fixed test * fixed telemetry for throttle and interval * fixed task data * fixed task data * fixed test * fixed due to comments * fixed typecheck * fixed test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/usage/alerts_telemetry.test.ts | 33 +- .../alerting/server/usage/alerts_telemetry.ts | 324 ++++++------------ 2 files changed, 128 insertions(+), 229 deletions(-) diff --git a/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts b/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts index 348036252817d..03a96d19b8e8a 100644 --- a/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts +++ b/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts @@ -52,7 +52,7 @@ Object { `); }); - test('getTotalCountAggregations should return aggregations for throttle, interval and associated actions', async () => { + test('getTotalCountAggregations should return min/max connectors in use', async () => { const mockEsClient = elasticsearchClientMock.createClusterClient().asScoped().asInternalUser; mockEsClient.search.mockReturnValue( // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values @@ -65,18 +65,17 @@ Object { 'logs.alert.document.count': 1, 'document.test.': 1, }, - namespaces: { - default: 1, - }, - }, - }, - throttleTime: { value: { min: 0, max: 10, totalCount: 10, totalSum: 20 } }, - intervalTime: { value: { min: 0, max: 2, totalCount: 2, totalSum: 5 } }, - connectorsAgg: { - connectors: { - value: { min: 0, max: 5, totalActionsCount: 10, totalAlertsCount: 2 }, }, }, + max_throttle_time: { value: 60 }, + min_throttle_time: { value: 0 }, + avg_throttle_time: { value: 30 }, + max_interval_time: { value: 10 }, + min_interval_time: { value: 1 }, + avg_interval_time: { value: 4.5 }, + max_actions_count: { value: 4 }, + min_actions_count: { value: 0 }, + avg_actions_count: { value: 2.5 }, }, hits: { hits: [], @@ -92,7 +91,7 @@ Object { Object { "connectors_per_alert": Object { "avg": 2.5, - "max": 5, + "max": 4, "min": 0, }, "count_by_type": Object { @@ -103,13 +102,13 @@ Object { "count_rules_namespaces": 0, "count_total": 4, "schedule_time": Object { - "avg": 2.5, - "max": 2, - "min": 0, + "avg": 4.5, + "max": 10, + "min": 1, }, "throttle_time": Object { - "avg": 2, - "max": 10, + "avg": 30, + "max": 60, "min": 0, }, } diff --git a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts b/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts index ede2ac3613296..7ff9538c1aa26 100644 --- a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts +++ b/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts @@ -52,219 +52,128 @@ export async function getTotalCountAggregations( | 'count_rules_namespaces' > > { - const throttleTimeMetric = { - scripted_metric: { - init_script: 'state.min = 0; state.max = 0; state.totalSum = 0; state.totalCount = 0;', - map_script: ` - if (doc['alert.throttle'].size() > 0) { - def throttle = doc['alert.throttle'].value; + const { body: results } = await esClient.search({ + index: kibanaInex, + body: { + size: 0, + query: { + bool: { + filter: [{ term: { type: 'alert' } }], + }, + }, + runtime_mappings: { + alert_action_count: { + type: 'long', + script: { + source: ` + def alert = params._source['alert']; + if (alert != null) { + def actions = alert.actions; + if (actions != null) { + emit(actions.length); + } else { + emit(0); + } + }`, + }, + }, + alert_interval: { + type: 'long', + script: { + source: ` + int parsed = 0; + if (doc['alert.schedule.interval'].size() > 0) { + def interval = doc['alert.schedule.interval'].value; - if (throttle.length() > 1) { - // get last char - String timeChar = throttle.substring(throttle.length() - 1); - // remove last char - throttle = throttle.substring(0, throttle.length() - 1); + if (interval.length() > 1) { + // get last char + String timeChar = interval.substring(interval.length() - 1); + // remove last char + interval = interval.substring(0, interval.length() - 1); - if (throttle.chars().allMatch(Character::isDigit)) { - // using of regex is not allowed in painless language - int parsed = Integer.parseInt(throttle); + if (interval.chars().allMatch(Character::isDigit)) { + // using of regex is not allowed in painless language + parsed = Integer.parseInt(interval); - if (timeChar.equals("s")) { - parsed = parsed; - } else if (timeChar.equals("m")) { - parsed = parsed * 60; - } else if (timeChar.equals("h")) { - parsed = parsed * 60 * 60; - } else if (timeChar.equals("d")) { - parsed = parsed * 24 * 60 * 60; - } - if (state.min === 0 || parsed < state.min) { - state.min = parsed; - } - if (parsed > state.max) { - state.max = parsed; + if (timeChar.equals("s")) { + parsed = parsed; + } else if (timeChar.equals("m")) { + parsed = parsed * 60; + } else if (timeChar.equals("h")) { + parsed = parsed * 60 * 60; + } else if (timeChar.equals("d")) { + parsed = parsed * 24 * 60 * 60; + } + emit(parsed); + } } - state.totalSum += parsed; - state.totalCount++; } - } - } - `, - // Combine script is executed per cluster, but we already have a key-value pair per cluster. - // Despite docs that say this is optional, this script can't be blank. - combine_script: 'return state', - // Reduce script is executed across all clusters, so we need to add up all the total from each cluster - // This also needs to account for having no data - reduce_script: ` - double min = 0; - double max = 0; - long totalSum = 0; - long totalCount = 0; - for (Map m : states.toArray()) { - if (m !== null) { - min = min > 0 ? Math.min(min, m.min) : m.min; - max = Math.max(max, m.max); - totalSum += m.totalSum; - totalCount += m.totalCount; - } - } - Map result = new HashMap(); - result.min = min; - result.max = max; - result.totalSum = totalSum; - result.totalCount = totalCount; - return result; - `, - }, - }; - - const intervalTimeMetric = { - scripted_metric: { - init_script: 'state.min = 0; state.max = 0; state.totalSum = 0; state.totalCount = 0;', - map_script: ` - if (doc['alert.schedule.interval'].size() > 0) { - def interval = doc['alert.schedule.interval'].value; + emit(parsed); + `, + }, + }, + alert_throttle: { + type: 'long', + script: { + source: ` + int parsed = 0; + if (doc['alert.throttle'].size() > 0) { + def throttle = doc['alert.throttle'].value; - if (interval.length() > 1) { - // get last char - String timeChar = interval.substring(interval.length() - 1); - // remove last char - interval = interval.substring(0, interval.length() - 1); + if (throttle.length() > 1) { + // get last char + String timeChar = throttle.substring(throttle.length() - 1); + // remove last char + throttle = throttle.substring(0, throttle.length() - 1); - if (interval.chars().allMatch(Character::isDigit)) { - // using of regex is not allowed in painless language - int parsed = Integer.parseInt(interval); + if (throttle.chars().allMatch(Character::isDigit)) { + // using of regex is not allowed in painless language + parsed = Integer.parseInt(throttle); - if (timeChar.equals("s")) { - parsed = parsed; - } else if (timeChar.equals("m")) { - parsed = parsed * 60; - } else if (timeChar.equals("h")) { - parsed = parsed * 60 * 60; - } else if (timeChar.equals("d")) { - parsed = parsed * 24 * 60 * 60; - } - if (state.min === 0 || parsed < state.min) { - state.min = parsed; - } - if (parsed > state.max) { - state.max = parsed; - } - state.totalSum += parsed; - state.totalCount++; + if (timeChar.equals("s")) { + parsed = parsed; + } else if (timeChar.equals("m")) { + parsed = parsed * 60; + } else if (timeChar.equals("h")) { + parsed = parsed * 60 * 60; + } else if (timeChar.equals("d")) { + parsed = parsed * 24 * 60 * 60; + } + emit(parsed); + } } - } - } - `, - // Combine script is executed per cluster, but we already have a key-value pair per cluster. - // Despite docs that say this is optional, this script can't be blank. - combine_script: 'return state', - // Reduce script is executed across all clusters, so we need to add up all the total from each cluster - // This also needs to account for having no data - reduce_script: ` - double min = 0; - double max = 0; - long totalSum = 0; - long totalCount = 0; - for (Map m : states.toArray()) { - if (m !== null) { - min = min > 0 ? Math.min(min, m.min) : m.min; - max = Math.max(max, m.max); - totalSum += m.totalSum; - totalCount += m.totalCount; - } - } - Map result = new HashMap(); - result.min = min; - result.max = max; - result.totalSum = totalSum; - result.totalCount = totalCount; - return result; - `, - }, - }; - - const connectorsMetric = { - scripted_metric: { - init_script: - 'state.currentAlertActions = 0; state.min = 0; state.max = 0; state.totalActionsCount = 0;', - map_script: ` - String refName = doc['alert.actions.actionRef'].value; - if (refName == 'action_0') { - if (state.currentAlertActions !== 0 && state.currentAlertActions < state.min) { - state.min = state.currentAlertActions; - } - if (state.currentAlertActions !== 0 && state.currentAlertActions > state.max) { - state.max = state.currentAlertActions; - } - state.currentAlertActions = 1; - } else { - state.currentAlertActions++; - } - state.totalActionsCount++; - `, - // Combine script is executed per cluster, but we already have a key-value pair per cluster. - // Despite docs that say this is optional, this script can't be blank. - combine_script: 'return state', - // Reduce script is executed across all clusters, so we need to add up all the total from each cluster - // This also needs to account for having no data - reduce_script: ` - double min = 0; - double max = 0; - long totalActionsCount = 0; - long currentAlertActions = 0; - for (Map m : states.toArray()) { - if (m !== null) { - min = min > 0 ? Math.min(min, m.min) : m.min; - max = Math.max(max, m.max); - currentAlertActions += m.currentAlertActions; - totalActionsCount += m.totalActionsCount; - } - } - Map result = new HashMap(); - result.min = min; - result.max = max; - result.currentAlertActions = currentAlertActions; - result.totalActionsCount = totalActionsCount; - return result; - `, - }, - }; - - const { body: results } = await esClient.search({ - index: kibanaInex, - size: 0, - body: { - query: { - bool: { - filter: [{ term: { type: 'alert' } }], + } + emit(parsed); + `, + }, }, }, aggs: { byAlertTypeId: alertTypeMetric, - throttleTime: throttleTimeMetric, - intervalTime: intervalTimeMetric, - connectorsAgg: { - nested: { - path: 'alert.actions', - }, - aggs: { - connectors: connectorsMetric, - }, - }, + max_throttle_time: { max: { field: 'alert_throttle' } }, + min_throttle_time: { min: { field: 'alert_throttle' } }, + avg_throttle_time: { avg: { field: 'alert_throttle' } }, + max_interval_time: { max: { field: 'alert_interval' } }, + min_interval_time: { min: { field: 'alert_interval' } }, + avg_interval_time: { avg: { field: 'alert_interval' } }, + max_actions_count: { max: { field: 'alert_action_count' } }, + min_actions_count: { min: { field: 'alert_action_count' } }, + avg_actions_count: { avg: { field: 'alert_action_count' } }, }, }, }); const aggregations = results.aggregations as { byAlertTypeId: { value: { ruleTypes: Record } }; - throttleTime: { value: { min: number; max: number; totalCount: number; totalSum: number } }; - intervalTime: { value: { min: number; max: number; totalCount: number; totalSum: number } }; - connectorsAgg: { - connectors: { - value: { min: number; max: number; totalActionsCount: number; totalAlertsCount: number }; - }; - }; + max_throttle_time: { value: number }; + min_throttle_time: { value: number }; + avg_throttle_time: { value: number }; + max_interval_time: { value: number }; + min_interval_time: { value: number }; + avg_interval_time: { value: number }; + max_actions_count: { value: number }; + min_actions_count: { value: number }; + avg_actions_count: { value: number }; }; const totalAlertsCount = Object.keys(aggregations.byAlertTypeId.value.ruleTypes).reduce( @@ -285,28 +194,19 @@ export async function getTotalCountAggregations( {} ), throttle_time: { - min: aggregations.throttleTime.value.min, - avg: - aggregations.throttleTime.value.totalCount > 0 - ? aggregations.throttleTime.value.totalSum / aggregations.throttleTime.value.totalCount - : 0, - max: aggregations.throttleTime.value.max, + min: aggregations.min_throttle_time.value, + avg: aggregations.avg_throttle_time.value, + max: aggregations.max_throttle_time.value, }, schedule_time: { - min: aggregations.intervalTime.value.min, - avg: - aggregations.intervalTime.value.totalCount > 0 - ? aggregations.intervalTime.value.totalSum / aggregations.intervalTime.value.totalCount - : 0, - max: aggregations.intervalTime.value.max, + min: aggregations.min_interval_time.value, + avg: aggregations.avg_interval_time.value, + max: aggregations.max_interval_time.value, }, connectors_per_alert: { - min: aggregations.connectorsAgg.connectors.value.min, - avg: - totalAlertsCount > 0 - ? aggregations.connectorsAgg.connectors.value.totalActionsCount / totalAlertsCount - : 0, - max: aggregations.connectorsAgg.connectors.value.max, + min: aggregations.min_actions_count.value, + avg: aggregations.avg_actions_count.value, + max: aggregations.max_actions_count.value, }, count_rules_namespaces: 0, }; From 776ad4896b04730265b0bb065a621927a8f52b0b Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Thu, 21 Oct 2021 20:23:21 -0700 Subject: [PATCH 37/40] Removing tests that support legacy exports (#116011) * deleted 2 files * removing the references of the deleted file --- test/functional/apps/dashboard/bwc_import.ts | 43 ------------- test/functional/apps/dashboard/index.ts | 2 - test/functional/apps/dashboard/time_zones.ts | 68 -------------------- 3 files changed, 113 deletions(-) delete mode 100644 test/functional/apps/dashboard/bwc_import.ts delete mode 100644 test/functional/apps/dashboard/time_zones.ts diff --git a/test/functional/apps/dashboard/bwc_import.ts b/test/functional/apps/dashboard/bwc_import.ts deleted file mode 100644 index ebb9d2b99ffa7..0000000000000 --- a/test/functional/apps/dashboard/bwc_import.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import path from 'path'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['dashboard', 'header', 'settings', 'savedObjects', 'common']); - const dashboardExpect = getService('dashboardExpect'); - // Legacy imports are no longer supported https://github.com/elastic/kibana/issues/103921 - describe.skip('bwc import', function describeIndexTests() { - before(async function () { - await PageObjects.dashboard.initTests(); - await PageObjects.settings.navigateTo(); - await PageObjects.settings.clickKibanaSavedObjects(); - await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', 'dashboard_6_0_1.json') - ); - await PageObjects.settings.associateIndexPattern( - 'dd684000-8255-11eb-a5e7-93c302c8f329', - 'logstash-*' - ); - await PageObjects.savedObjects.clickConfirmChanges(); - await PageObjects.savedObjects.clickImportDone(); - await PageObjects.common.navigateToApp('dashboard'); - }); - - describe('6.0.1 dashboard', () => { - it('loads an imported dashboard', async function () { - await PageObjects.dashboard.gotoDashboardLandingPage(); - await PageObjects.dashboard.loadSavedDashboard('My custom bwc dashboard'); - await PageObjects.header.waitUntilLoadingHasFinished(); - - await dashboardExpect.metricValuesExist(['14,004']); - }); - }); - }); -} diff --git a/test/functional/apps/dashboard/index.ts b/test/functional/apps/dashboard/index.ts index 8627a258869bb..c9a62447f223a 100644 --- a/test/functional/apps/dashboard/index.ts +++ b/test/functional/apps/dashboard/index.ts @@ -49,7 +49,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./dashboard_unsaved_state')); loadTestFile(require.resolve('./dashboard_unsaved_listing')); loadTestFile(require.resolve('./edit_visualizations')); - loadTestFile(require.resolve('./time_zones')); loadTestFile(require.resolve('./dashboard_options')); loadTestFile(require.resolve('./data_shared_attributes')); loadTestFile(require.resolve('./share')); @@ -95,7 +94,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./dashboard_time_picker')); loadTestFile(require.resolve('./bwc_shared_urls')); - loadTestFile(require.resolve('./bwc_import')); loadTestFile(require.resolve('./panel_replacing')); loadTestFile(require.resolve('./panel_cloning')); loadTestFile(require.resolve('./copy_panel_to')); diff --git a/test/functional/apps/dashboard/time_zones.ts b/test/functional/apps/dashboard/time_zones.ts deleted file mode 100644 index f60792b3f292a..0000000000000 --- a/test/functional/apps/dashboard/time_zones.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import path from 'path'; -import expect from '@kbn/expect'; - -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const pieChart = getService('pieChart'); - const esArchiver = getService('esArchiver'); - const kibanaServer = getService('kibanaServer'); - const PageObjects = getPageObjects([ - 'dashboard', - 'timePicker', - 'settings', - 'common', - 'savedObjects', - ]); - // Legacy imports are no longer supported https://github.com/elastic/kibana/issues/103921 - describe.skip('dashboard time zones', function () { - this.tags('includeFirefox'); - - before(async () => { - await esArchiver.load('test/functional/fixtures/es_archiver/dashboard/current/kibana'); - await kibanaServer.uiSettings.replace({ - defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', - }); - await PageObjects.settings.navigateTo(); - await PageObjects.settings.clickKibanaSavedObjects(); - await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', 'timezonetest_6_2_4.json') - ); - await PageObjects.savedObjects.checkImportSucceeded(); - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.preserveCrossAppState(); - await PageObjects.dashboard.loadSavedDashboard('time zone test'); - }); - - after(async () => { - await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'UTC' }); - }); - - it('Exported dashboard adjusts EST time to UTC', async () => { - const time = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); - expect(time.start).to.be('Apr 10, 2018 @ 03:00:00.000'); - expect(time.end).to.be('Apr 10, 2018 @ 04:00:00.000'); - await pieChart.expectPieSliceCount(4); - }); - - it('Changing timezone changes dashboard timestamp and shows the same data', async () => { - await PageObjects.settings.navigateTo(); - await PageObjects.settings.clickKibanaSettings(); - await PageObjects.settings.setAdvancedSettingsSelect('dateFormat:tz', 'Etc/GMT+5'); - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.loadSavedDashboard('time zone test'); - const time = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); - expect(time.start).to.be('Apr 9, 2018 @ 22:00:00.000'); - expect(time.end).to.be('Apr 9, 2018 @ 23:00:00.000'); - await pieChart.expectPieSliceCount(4); - }); - }); -} From 78a91f7595cd1879ca2f2b4ef0bd331380dce694 Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Fri, 22 Oct 2021 07:14:45 -0600 Subject: [PATCH 38/40] Remove migrations.enableV2 in 8.0 (#116023) --- .../saved_objects_config.test.ts | 44 ------------------- .../saved_objects/saved_objects_config.ts | 4 -- 2 files changed, 48 deletions(-) delete mode 100644 src/core/server/saved_objects/saved_objects_config.test.ts diff --git a/src/core/server/saved_objects/saved_objects_config.test.ts b/src/core/server/saved_objects/saved_objects_config.test.ts deleted file mode 100644 index 06b9e9661b746..0000000000000 --- a/src/core/server/saved_objects/saved_objects_config.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { savedObjectsMigrationConfig } from './saved_objects_config'; -import { getDeprecationsFor } from '../config/test_utils'; - -const applyMigrationsDeprecations = (settings: Record = {}) => - getDeprecationsFor({ - provider: savedObjectsMigrationConfig.deprecations!, - settings, - path: 'migrations', - }); - -describe('migrations config', function () { - describe('deprecations', () => { - it('logs a warning if migrations.enableV2 is set: true', () => { - const { messages } = applyMigrationsDeprecations({ enableV2: true }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "You no longer need to configure \\"migrations.enableV2\\".", - ] - `); - }); - - it('logs a warning if migrations.enableV2 is set: false', () => { - const { messages } = applyMigrationsDeprecations({ enableV2: false }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "You no longer need to configure \\"migrations.enableV2\\".", - ] - `); - }); - }); - - it('does not log a warning if migrations.enableV2 is not set', () => { - const { messages } = applyMigrationsDeprecations({ batchSize: 1_000 }); - expect(messages).toMatchInlineSnapshot(`Array []`); - }); -}); diff --git a/src/core/server/saved_objects/saved_objects_config.ts b/src/core/server/saved_objects/saved_objects_config.ts index 02fbd974da4ae..e5dc64186f66d 100644 --- a/src/core/server/saved_objects/saved_objects_config.ts +++ b/src/core/server/saved_objects/saved_objects_config.ts @@ -7,7 +7,6 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { ConfigDeprecationProvider } from '../config'; import type { ServiceConfigDescriptor } from '../internal_types'; const migrationSchema = schema.object({ @@ -21,13 +20,10 @@ const migrationSchema = schema.object({ export type SavedObjectsMigrationConfigType = TypeOf; -const migrationDeprecations: ConfigDeprecationProvider = ({ unused }) => [unused('enableV2')]; - export const savedObjectsMigrationConfig: ServiceConfigDescriptor = { path: 'migrations', schema: migrationSchema, - deprecations: migrationDeprecations, }; const soSchema = schema.object({ From 003c0f36ec178dcde1d36679e49477a27065c1fa Mon Sep 17 00:00:00 2001 From: Sandra G Date: Fri, 22 Oct 2021 14:17:12 -0400 Subject: [PATCH 39/40] don't request when request is pending (#115999) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/application/pages/page_template.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/monitoring/public/application/pages/page_template.tsx b/x-pack/plugins/monitoring/public/application/pages/page_template.tsx index c0030cfcfe55c..a508714612c28 100644 --- a/x-pack/plugins/monitoring/public/application/pages/page_template.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/page_template.tsx @@ -49,6 +49,7 @@ export const PageTemplate: React.FC = ({ const { currentTimerange } = useContext(MonitoringTimeContainer.Context); const [loaded, setLoaded] = useState(false); + const [isRequestPending, setIsRequestPending] = useState(false); const history = useHistory(); const [hasError, setHasError] = useState(false); const handleRequestError = useRequestErrorHandler(); @@ -62,6 +63,7 @@ export const PageTemplate: React.FC = ({ ); useEffect(() => { + setIsRequestPending(true); getPageData?.() .then(getPageDataResponseHandler) .catch((err: IHttpFetchError) => { @@ -70,11 +72,20 @@ export const PageTemplate: React.FC = ({ }) .finally(() => { setLoaded(true); + setIsRequestPending(false); }); }, [getPageData, currentTimerange, getPageDataResponseHandler, handleRequestError]); const onRefresh = () => { - getPageData?.().then(getPageDataResponseHandler).catch(handleRequestError); + // don't refresh when a request is pending + if (isRequestPending) return; + setIsRequestPending(true); + getPageData?.() + .then(getPageDataResponseHandler) + .catch(handleRequestError) + .finally(() => { + setIsRequestPending(false); + }); if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { updateSetupModeData(); From 110a8418f9568624ab2e1764e2ed78d16ee3d9a7 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Sat, 23 Oct 2021 23:54:21 +0200 Subject: [PATCH 40/40] [APM] Add live mode to synthtrace (#115988) --- packages/elastic-apm-generator/BUILD.bazel | 2 + packages/elastic-apm-generator/README.md | 28 +++-- .../elastic-apm-generator/src/.eslintrc.js | 13 ++ .../elastic-apm-generator/src/lib/interval.ts | 2 +- .../src/lib/output/to_elasticsearch_output.ts | 22 +++- .../elastic-apm-generator/src/scripts/es.ts | 113 ----------------- .../src/scripts/examples/01_simple_trace.ts | 4 +- .../src/scripts/{es.js => run.js} | 2 +- .../elastic-apm-generator/src/scripts/run.ts | 117 ++++++++++++++++++ .../src/scripts/utils/clean_write_targets.ts | 63 ++++++++++ .../src/scripts/utils/common_options.ts | 53 ++++++++ .../src/scripts/utils/get_common_resources.ts | 80 ++++++++++++ .../src/scripts/utils/get_scenario.ts | 25 ++++ .../src/scripts/utils/get_write_targets.ts | 56 +++++++++ .../src/scripts/utils/interval_to_ms.ts | 31 +++++ .../src/scripts/utils/logger.ts | 32 +++++ .../utils/start_historical_data_upload.ts | 64 ++++++++++ .../scripts/utils/start_live_data_upload.ts | 75 +++++++++++ .../src/scripts/utils/upload_events.ts | 72 +++++++++++ .../test/scenarios/01_simple_trace.test.ts | 2 +- .../scenarios/02_transaction_metrics.test.ts | 2 +- .../03_span_destination_metrics.test.ts | 2 +- .../scenarios/04_breakdown_metrics.test.ts | 2 +- .../src/test/to_elasticsearch_output.test.ts | 11 +- .../apm_api_integration/common/trace_data.ts | 13 +- 25 files changed, 750 insertions(+), 136 deletions(-) create mode 100644 packages/elastic-apm-generator/src/.eslintrc.js delete mode 100644 packages/elastic-apm-generator/src/scripts/es.ts rename packages/elastic-apm-generator/src/scripts/{es.js => run.js} (96%) create mode 100644 packages/elastic-apm-generator/src/scripts/run.ts create mode 100644 packages/elastic-apm-generator/src/scripts/utils/clean_write_targets.ts create mode 100644 packages/elastic-apm-generator/src/scripts/utils/common_options.ts create mode 100644 packages/elastic-apm-generator/src/scripts/utils/get_common_resources.ts create mode 100644 packages/elastic-apm-generator/src/scripts/utils/get_scenario.ts create mode 100644 packages/elastic-apm-generator/src/scripts/utils/get_write_targets.ts create mode 100644 packages/elastic-apm-generator/src/scripts/utils/interval_to_ms.ts create mode 100644 packages/elastic-apm-generator/src/scripts/utils/logger.ts create mode 100644 packages/elastic-apm-generator/src/scripts/utils/start_historical_data_upload.ts create mode 100644 packages/elastic-apm-generator/src/scripts/utils/start_live_data_upload.ts create mode 100644 packages/elastic-apm-generator/src/scripts/utils/upload_events.ts diff --git a/packages/elastic-apm-generator/BUILD.bazel b/packages/elastic-apm-generator/BUILD.bazel index 6b46b2b9181e5..396c27b3a4c89 100644 --- a/packages/elastic-apm-generator/BUILD.bazel +++ b/packages/elastic-apm-generator/BUILD.bazel @@ -25,6 +25,7 @@ NPM_MODULE_EXTRA_FILES = [ ] RUNTIME_DEPS = [ + "//packages/elastic-datemath", "@npm//@elastic/elasticsearch", "@npm//lodash", "@npm//moment", @@ -36,6 +37,7 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ + "//packages/elastic-datemath", "@npm//@elastic/elasticsearch", "@npm//moment", "@npm//p-limit", diff --git a/packages/elastic-apm-generator/README.md b/packages/elastic-apm-generator/README.md index e43187a8155d3..b442c0ec23ee0 100644 --- a/packages/elastic-apm-generator/README.md +++ b/packages/elastic-apm-generator/README.md @@ -11,7 +11,7 @@ This section assumes that you've installed Kibana's dependencies by running `yar This library can currently be used in two ways: - Imported as a Node.js module, for instance to be used in Kibana's functional test suite. -- With a command line interface, to index data based on some example scenarios. +- With a command line interface, to index data based on a specified scenario. ### Using the Node.js module @@ -32,7 +32,7 @@ const instance = service('synth-go', 'production', 'go') .instance('instance-a'); const from = new Date('2021-01-01T12:00:00.000Z').getTime(); -const to = new Date('2021-01-01T12:00:00.000Z').getTime() - 1; +const to = new Date('2021-01-01T12:00:00.000Z').getTime(); const traceEvents = timerange(from, to) .interval('1m') @@ -82,12 +82,26 @@ const esEvents = toElasticsearchOutput([ ### CLI -Via the CLI, you can upload examples. The supported examples are listed in `src/lib/es.ts`. A `--target` option that specifies the Elasticsearch URL should be defined when running the `example` command. Here's an example: +Via the CLI, you can upload scenarios, either using a fixed time range or continuously generating data. Some examples are available in in `src/scripts/examples`. Here's an example for live data: -`$ node packages/elastic-apm-generator/src/scripts/es.js example simple-trace --target=http://admin:changeme@localhost:9200` +`$ node packages/elastic-apm-generator/src/scripts/run packages/elastic-apm-generator/src/examples/01_simple_trace.ts --target=http://admin:changeme@localhost:9200 --live` + +For a fixed time window: +`$ node packages/elastic-apm-generator/src/scripts/run packages/elastic-apm-generator/src/examples/01_simple_trace.ts --target=http://admin:changeme@localhost:9200 --from=now-24h --to=now` + +The script will try to automatically find bootstrapped APM indices. __If these indices do not exist, the script will exit with an error. It will not bootstrap the indices itself.__ The following options are supported: -- `to`: the end of the time range, in ISO format. By default, the current time will be used. -- `from`: the start of the time range, in ISO format. By default, `to` minus 15 minutes will be used. -- `apm-server-version`: the version used in the index names bootstrapped by APM Server, e.g. `7.16.0`. __If these indices do not exist, the script will exit with an error. It will not bootstrap the indices itself.__ +| Option | Description | Default | +| -------------- | ------------------------------------------------------- | ------------ | +| `--from` | The start of the time window. | `now - 15m` | +| `--to` | The end of the time window. | `now` | +| `--live` | Continously ingest data | `false` | +| `--bucketSize` | Size of bucket for which to generate data. | `15m` | +| `--clean` | Clean APM indices before indexing new data. | `false` | +| `--interval` | The interval at which to index data. | `10s` | +| `--logLevel` | Log level. | `info` | +| `--lookback` | The lookback window for which data should be generated. | `15m` | +| `--target` | Elasticsearch target, including username/password. | **Required** | +| `--workers` | Amount of simultaneously connected ES clients. | `1` | diff --git a/packages/elastic-apm-generator/src/.eslintrc.js b/packages/elastic-apm-generator/src/.eslintrc.js new file mode 100644 index 0000000000000..2e3eef95f4bf3 --- /dev/null +++ b/packages/elastic-apm-generator/src/.eslintrc.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + rules: { + 'import/no-default-export': 'off', + }, +}; diff --git a/packages/elastic-apm-generator/src/lib/interval.ts b/packages/elastic-apm-generator/src/lib/interval.ts index f13d54fd7415e..bafd1a06c5348 100644 --- a/packages/elastic-apm-generator/src/lib/interval.ts +++ b/packages/elastic-apm-generator/src/lib/interval.ts @@ -21,7 +21,7 @@ export class Interval { throw new Error('Failed to parse interval'); } const timestamps: number[] = []; - while (now <= this.to) { + while (now < this.to) { timestamps.push(...new Array(rate).fill(now)); now = moment(now) .add(Number(args[1]), args[2] as any) diff --git a/packages/elastic-apm-generator/src/lib/output/to_elasticsearch_output.ts b/packages/elastic-apm-generator/src/lib/output/to_elasticsearch_output.ts index d90ce8e01f83d..31f3e8c8ed270 100644 --- a/packages/elastic-apm-generator/src/lib/output/to_elasticsearch_output.ts +++ b/packages/elastic-apm-generator/src/lib/output/to_elasticsearch_output.ts @@ -10,7 +10,25 @@ import { set } from 'lodash'; import { getObserverDefaults } from '../..'; import { Fields } from '../entity'; -export function toElasticsearchOutput(events: Fields[], versionOverride?: string) { +export interface ElasticsearchOutput { + _index: string; + _source: unknown; +} + +export interface ElasticsearchOutputWriteTargets { + transaction: string; + span: string; + error: string; + metric: string; +} + +export function toElasticsearchOutput({ + events, + writeTargets, +}: { + events: Fields[]; + writeTargets: ElasticsearchOutputWriteTargets; +}): ElasticsearchOutput[] { return events.map((event) => { const values = { ...event, @@ -29,7 +47,7 @@ export function toElasticsearchOutput(events: Fields[], versionOverride?: string set(document, key, val); } return { - _index: `apm-${versionOverride || values['observer.version']}-${values['processor.event']}`, + _index: writeTargets[event['processor.event'] as keyof ElasticsearchOutputWriteTargets], _source: document, }; }); diff --git a/packages/elastic-apm-generator/src/scripts/es.ts b/packages/elastic-apm-generator/src/scripts/es.ts deleted file mode 100644 index d023ef7172892..0000000000000 --- a/packages/elastic-apm-generator/src/scripts/es.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { inspect } from 'util'; -import { Client } from '@elastic/elasticsearch'; -import { chunk } from 'lodash'; -import pLimit from 'p-limit'; -import yargs from 'yargs/yargs'; -import { toElasticsearchOutput } from '..'; -import { simpleTrace } from './examples/01_simple_trace'; - -yargs(process.argv.slice(2)) - .command( - 'example', - 'run an example scenario', - (y) => { - return y - .positional('scenario', { - describe: 'scenario to run', - choices: ['simple-trace'], - demandOption: true, - }) - .option('target', { - describe: 'elasticsearch target, including username/password', - }) - .option('from', { describe: 'start of timerange' }) - .option('to', { describe: 'end of timerange' }) - .option('workers', { - default: 1, - describe: 'number of concurrently connected ES clients', - }) - .option('apm-server-version', { - describe: 'APM Server version override', - }) - .demandOption('target'); - }, - (argv) => { - let events: any[] = []; - const toDateString = (argv.to as string | undefined) || new Date().toISOString(); - const fromDateString = - (argv.from as string | undefined) || - new Date(new Date(toDateString).getTime() - 15 * 60 * 1000).toISOString(); - - const to = new Date(toDateString).getTime(); - const from = new Date(fromDateString).getTime(); - - switch (argv._[1]) { - case 'simple-trace': - events = simpleTrace(from, to); - break; - } - - const docs = toElasticsearchOutput(events, argv['apm-server-version'] as string); - - const client = new Client({ - node: argv.target as string, - }); - - const fn = pLimit(argv.workers); - - const batches = chunk(docs, 1000); - - // eslint-disable-next-line no-console - console.log( - 'Uploading', - docs.length, - 'docs in', - batches.length, - 'batches', - 'from', - fromDateString, - 'to', - toDateString - ); - - Promise.all( - batches.map((batch) => - fn(() => { - return client.bulk({ - require_alias: true, - body: batch.flatMap((doc) => { - return [{ index: { _index: doc._index } }, doc._source]; - }), - }); - }) - ) - ) - .then((results) => { - const errors = results - .flatMap((result) => result.body.items) - .filter((item) => !!item.index?.error) - .map((item) => item.index?.error); - - if (errors.length) { - // eslint-disable-next-line no-console - console.error(inspect(errors.slice(0, 10), { depth: null })); - throw new Error('Failed to upload some items'); - } - process.exit(); - }) - .catch((err) => { - // eslint-disable-next-line no-console - console.error(err); - process.exit(1); - }); - } - ) - .parse(); diff --git a/packages/elastic-apm-generator/src/scripts/examples/01_simple_trace.ts b/packages/elastic-apm-generator/src/scripts/examples/01_simple_trace.ts index f6aad154532c2..6b857391b4f96 100644 --- a/packages/elastic-apm-generator/src/scripts/examples/01_simple_trace.ts +++ b/packages/elastic-apm-generator/src/scripts/examples/01_simple_trace.ts @@ -9,12 +9,12 @@ import { service, timerange, getTransactionMetrics, getSpanDestinationMetrics } from '../..'; import { getBreakdownMetrics } from '../../lib/utils/get_breakdown_metrics'; -export function simpleTrace(from: number, to: number) { +export default function ({ from, to }: { from: number; to: number }) { const instance = service('opbeans-go', 'production', 'go').instance('instance'); const range = timerange(from, to); - const transactionName = '240rpm/60% 1000ms'; + const transactionName = '240rpm/75% 1000ms'; const successfulTraceEvents = range .interval('1s') diff --git a/packages/elastic-apm-generator/src/scripts/es.js b/packages/elastic-apm-generator/src/scripts/run.js similarity index 96% rename from packages/elastic-apm-generator/src/scripts/es.js rename to packages/elastic-apm-generator/src/scripts/run.js index 9f99a5d19b8f8..426b247b6b623 100644 --- a/packages/elastic-apm-generator/src/scripts/es.js +++ b/packages/elastic-apm-generator/src/scripts/run.js @@ -12,4 +12,4 @@ require('@babel/register')({ presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], }); -require('./es.ts'); +require('./run.ts'); diff --git a/packages/elastic-apm-generator/src/scripts/run.ts b/packages/elastic-apm-generator/src/scripts/run.ts new file mode 100644 index 0000000000000..ad453ac96ff10 --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/run.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 datemath from '@elastic/datemath'; +import yargs from 'yargs/yargs'; +import { cleanWriteTargets } from './utils/clean_write_targets'; +import { + bucketSizeOption, + cleanOption, + fileOption, + intervalOption, + targetOption, + workerOption, + logLevelOption, +} from './utils/common_options'; +import { intervalToMs } from './utils/interval_to_ms'; +import { getCommonResources } from './utils/get_common_resources'; +import { startHistoricalDataUpload } from './utils/start_historical_data_upload'; +import { startLiveDataUpload } from './utils/start_live_data_upload'; + +yargs(process.argv.slice(2)) + .command( + '*', + 'Generate data and index into Elasticsearch', + (y) => { + return y + .positional('file', fileOption) + .option('bucketSize', bucketSizeOption) + .option('workers', workerOption) + .option('interval', intervalOption) + .option('clean', cleanOption) + .option('target', targetOption) + .option('logLevel', logLevelOption) + .option('from', { + description: 'The start of the time window', + }) + .option('to', { + description: 'The end of the time window', + }) + .option('live', { + description: 'Generate and index data continuously', + boolean: true, + }) + .conflicts('to', 'live'); + }, + async (argv) => { + const { + scenario, + intervalInMs, + bucketSizeInMs, + target, + workers, + clean, + logger, + writeTargets, + client, + } = await getCommonResources(argv); + + if (clean) { + await cleanWriteTargets({ writeTargets, client, logger }); + } + + const to = datemath.parse(String(argv.to ?? 'now'))!.valueOf(); + const from = argv.from + ? datemath.parse(String(argv.from))!.valueOf() + : to - intervalToMs('15m'); + + const live = argv.live; + + logger.info( + `Starting data generation\n: ${JSON.stringify( + { + intervalInMs, + bucketSizeInMs, + workers, + target, + writeTargets, + from: new Date(from).toISOString(), + to: new Date(to).toISOString(), + live, + }, + null, + 2 + )}` + ); + + startHistoricalDataUpload({ + from, + to, + scenario, + intervalInMs, + bucketSizeInMs, + client, + workers, + writeTargets, + logger, + }); + + if (live) { + startLiveDataUpload({ + bucketSizeInMs, + client, + intervalInMs, + logger, + scenario, + start: to, + workers, + writeTargets, + }); + } + } + ) + .parse(); diff --git a/packages/elastic-apm-generator/src/scripts/utils/clean_write_targets.ts b/packages/elastic-apm-generator/src/scripts/utils/clean_write_targets.ts new file mode 100644 index 0000000000000..efa24f164d51e --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/clean_write_targets.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { Client } from '@elastic/elasticsearch'; +import { ElasticsearchOutputWriteTargets } from '../../lib/output/to_elasticsearch_output'; +import { Logger } from './logger'; + +export async function cleanWriteTargets({ + writeTargets, + client, + logger, +}: { + writeTargets: ElasticsearchOutputWriteTargets; + client: Client; + logger: Logger; +}) { + const targets = Object.values(writeTargets); + + logger.info(`Cleaning indices: ${targets.join(', ')}`); + + const response = await client.deleteByQuery({ + index: targets, + allow_no_indices: true, + conflicts: 'proceed', + body: { + query: { + match_all: {}, + }, + }, + wait_for_completion: false, + }); + + const task = response.body.task; + + if (task) { + await new Promise((resolve, reject) => { + const pollForTaskCompletion = async () => { + const taskResponse = await client.tasks.get({ + task_id: String(task), + }); + + logger.debug( + `Polled for task:\n${JSON.stringify(taskResponse.body, ['completed', 'error'], 2)}` + ); + + if (taskResponse.body.completed) { + resolve(); + } else if (taskResponse.body.error) { + reject(taskResponse.body.error); + } else { + setTimeout(pollForTaskCompletion, 2500); + } + }; + + pollForTaskCompletion(); + }); + } +} diff --git a/packages/elastic-apm-generator/src/scripts/utils/common_options.ts b/packages/elastic-apm-generator/src/scripts/utils/common_options.ts new file mode 100644 index 0000000000000..eba547114d533 --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/common_options.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 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. + */ + +const fileOption = { + describe: 'File that contains the trace scenario', + demandOption: true, +}; + +const intervalOption = { + describe: 'The interval at which to index data', + default: '10s', +}; + +const targetOption = { + describe: 'Elasticsearch target, including username/password', + demandOption: true, +}; + +const bucketSizeOption = { + describe: 'Size of bucket for which to generate data', + default: '15m', +}; + +const workerOption = { + describe: 'Amount of simultaneously connected ES clients', + default: 1, +}; + +const cleanOption = { + describe: 'Clean APM indices before indexing new data', + default: false, + boolean: true as const, +}; + +const logLevelOption = { + describe: 'Log level', + default: 'info', +}; + +export { + fileOption, + intervalOption, + targetOption, + bucketSizeOption, + workerOption, + cleanOption, + logLevelOption, +}; diff --git a/packages/elastic-apm-generator/src/scripts/utils/get_common_resources.ts b/packages/elastic-apm-generator/src/scripts/utils/get_common_resources.ts new file mode 100644 index 0000000000000..1288c1390e92c --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/get_common_resources.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { Client } from '@elastic/elasticsearch'; +import { getScenario } from './get_scenario'; +import { getWriteTargets } from './get_write_targets'; +import { intervalToMs } from './interval_to_ms'; +import { createLogger, LogLevel } from './logger'; + +export async function getCommonResources({ + file, + interval, + bucketSize, + workers, + target, + clean, + logLevel, +}: { + file: unknown; + interval: unknown; + bucketSize: unknown; + workers: unknown; + target: unknown; + clean: boolean; + logLevel: unknown; +}) { + let parsedLogLevel = LogLevel.info; + switch (logLevel) { + case 'info': + parsedLogLevel = LogLevel.info; + break; + + case 'debug': + parsedLogLevel = LogLevel.debug; + break; + + case 'quiet': + parsedLogLevel = LogLevel.quiet; + break; + } + + const logger = createLogger(parsedLogLevel); + + const intervalInMs = intervalToMs(interval); + if (!intervalInMs) { + throw new Error('Invalid interval'); + } + + const bucketSizeInMs = intervalToMs(bucketSize); + + if (!bucketSizeInMs) { + throw new Error('Invalid bucket size'); + } + + const client = new Client({ + node: String(target), + }); + + const [scenario, writeTargets] = await Promise.all([ + getScenario({ file, logger }), + getWriteTargets({ client }), + ]); + + return { + scenario, + writeTargets, + logger, + client, + intervalInMs, + bucketSizeInMs, + workers: Number(workers), + target: String(target), + clean, + }; +} diff --git a/packages/elastic-apm-generator/src/scripts/utils/get_scenario.ts b/packages/elastic-apm-generator/src/scripts/utils/get_scenario.ts new file mode 100644 index 0000000000000..887969e8459cc --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/get_scenario.ts @@ -0,0 +1,25 @@ +/* + * 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 Path from 'path'; +import { Fields } from '../../lib/entity'; +import { Logger } from './logger'; + +export type Scenario = (options: { from: number; to: number }) => Fields[]; + +export function getScenario({ file, logger }: { file: unknown; logger: Logger }) { + const location = Path.join(process.cwd(), String(file)); + + logger.debug(`Loading scenario from ${location}`); + + return import(location).then((m) => { + if (m && m.default) { + return m.default; + } + throw new Error(`Could not find scenario at ${location}`); + }) as Promise; +} diff --git a/packages/elastic-apm-generator/src/scripts/utils/get_write_targets.ts b/packages/elastic-apm-generator/src/scripts/utils/get_write_targets.ts new file mode 100644 index 0000000000000..3640e4efaf796 --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/get_write_targets.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { Client } from '@elastic/elasticsearch'; +import { ElasticsearchOutputWriteTargets } from '../../lib/output/to_elasticsearch_output'; + +export async function getWriteTargets({ + client, +}: { + client: Client; +}): Promise { + const [indicesResponse, datastreamsResponse] = await Promise.all([ + client.indices.getAlias({ + index: 'apm-*', + }), + client.indices.getDataStream({ + name: '*apm', + }), + ]); + + function getDataStreamName(filter: string) { + return datastreamsResponse.body.data_streams.find((stream) => stream.name.includes(filter)) + ?.name; + } + + function getAlias(filter: string) { + return Object.keys(indicesResponse.body) + .map((key) => { + return { + key, + writeIndexAlias: Object.entries(indicesResponse.body[key].aliases).find( + ([_, alias]) => alias.is_write_index + )?.[0], + }; + }) + .find(({ key }) => key.includes(filter))?.writeIndexAlias!; + } + + const targets = { + transaction: getDataStreamName('traces-apm') || getAlias('-transaction'), + span: getDataStreamName('traces-apm') || getAlias('-span'), + metric: getDataStreamName('metrics-apm') || getAlias('-metric'), + error: getDataStreamName('logs-apm') || getAlias('-error'), + }; + + if (!targets.transaction || !targets.span || !targets.metric || !targets.error) { + throw new Error('Write targets could not be determined'); + } + + return targets; +} diff --git a/packages/elastic-apm-generator/src/scripts/utils/interval_to_ms.ts b/packages/elastic-apm-generator/src/scripts/utils/interval_to_ms.ts new file mode 100644 index 0000000000000..4cba832be3161 --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/interval_to_ms.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export function intervalToMs(interval: unknown) { + const [, valueAsString, unit] = String(interval).split(/(.*)(s|m|h|d|w)/); + + const value = Number(valueAsString); + + switch (unit) { + case 's': + return value * 1000; + case 'm': + return value * 1000 * 60; + + case 'h': + return value * 1000 * 60 * 60; + + case 'd': + return value * 1000 * 60 * 60 * 24; + + case 'w': + return value * 1000 * 60 * 60 * 24 * 7; + } + + throw new Error('Could not parse interval'); +} diff --git a/packages/elastic-apm-generator/src/scripts/utils/logger.ts b/packages/elastic-apm-generator/src/scripts/utils/logger.ts new file mode 100644 index 0000000000000..c9017cb08e663 --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/logger.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export enum LogLevel { + debug = 0, + info = 1, + quiet = 2, +} + +export function createLogger(logLevel: LogLevel) { + return { + debug: (...args: any[]) => { + if (logLevel <= LogLevel.debug) { + // eslint-disable-next-line no-console + console.debug(...args); + } + }, + info: (...args: any[]) => { + if (logLevel <= LogLevel.info) { + // eslint-disable-next-line no-console + console.log(...args); + } + }, + }; +} + +export type Logger = ReturnType; diff --git a/packages/elastic-apm-generator/src/scripts/utils/start_historical_data_upload.ts b/packages/elastic-apm-generator/src/scripts/utils/start_historical_data_upload.ts new file mode 100644 index 0000000000000..db14090dd1d8f --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/start_historical_data_upload.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { Client } from '@elastic/elasticsearch'; +import { ElasticsearchOutputWriteTargets } from '../../lib/output/to_elasticsearch_output'; +import { Scenario } from './get_scenario'; +import { Logger } from './logger'; +import { uploadEvents } from './upload_events'; + +export async function startHistoricalDataUpload({ + from, + to, + scenario, + intervalInMs, + bucketSizeInMs, + client, + workers, + writeTargets, + logger, +}: { + from: number; + to: number; + scenario: Scenario; + intervalInMs: number; + bucketSizeInMs: number; + client: Client; + workers: number; + writeTargets: ElasticsearchOutputWriteTargets; + logger: Logger; +}) { + let requestedUntil: number = from; + function uploadNextBatch() { + const bucketFrom = requestedUntil; + const bucketTo = Math.min(to, bucketFrom + bucketSizeInMs); + + const events = scenario({ from: bucketFrom, to: bucketTo }); + + logger.info( + `Uploading: ${new Date(bucketFrom).toISOString()} to ${new Date(bucketTo).toISOString()}` + ); + + uploadEvents({ + events, + client, + workers, + writeTargets, + logger, + }).then(() => { + if (bucketTo >= to) { + return; + } + uploadNextBatch(); + }); + + requestedUntil = bucketTo; + } + + return uploadNextBatch(); +} diff --git a/packages/elastic-apm-generator/src/scripts/utils/start_live_data_upload.ts b/packages/elastic-apm-generator/src/scripts/utils/start_live_data_upload.ts new file mode 100644 index 0000000000000..bf330732f343e --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/start_live_data_upload.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { Client } from '@elastic/elasticsearch'; +import { partition } from 'lodash'; +import { Fields } from '../../lib/entity'; +import { ElasticsearchOutputWriteTargets } from '../../lib/output/to_elasticsearch_output'; +import { Scenario } from './get_scenario'; +import { Logger } from './logger'; +import { uploadEvents } from './upload_events'; + +export function startLiveDataUpload({ + start, + bucketSizeInMs, + intervalInMs, + workers, + writeTargets, + scenario, + client, + logger, +}: { + start: number; + bucketSizeInMs: number; + intervalInMs: number; + workers: number; + writeTargets: ElasticsearchOutputWriteTargets; + scenario: Scenario; + client: Client; + logger: Logger; +}) { + let queuedEvents: Fields[] = []; + let requestedUntil: number = start; + + function uploadNextBatch() { + const end = new Date().getTime(); + if (end > requestedUntil) { + const bucketFrom = requestedUntil; + const bucketTo = requestedUntil + bucketSizeInMs; + const nextEvents = scenario({ from: bucketFrom, to: bucketTo }); + logger.debug( + `Requesting ${new Date(bucketFrom).toISOString()} to ${new Date( + bucketTo + ).toISOString()}, events: ${nextEvents.length}` + ); + queuedEvents.push(...nextEvents); + requestedUntil = bucketTo; + } + + const [eventsToUpload, eventsToRemainInQueue] = partition( + queuedEvents, + (event) => event['@timestamp']! <= end + ); + + logger.info(`Uploading until ${new Date(end).toISOString()}, events: ${eventsToUpload.length}`); + + queuedEvents = eventsToRemainInQueue; + + uploadEvents({ + events: eventsToUpload, + client, + workers, + writeTargets, + logger, + }); + } + + setInterval(uploadNextBatch, intervalInMs); + + uploadNextBatch(); +} diff --git a/packages/elastic-apm-generator/src/scripts/utils/upload_events.ts b/packages/elastic-apm-generator/src/scripts/utils/upload_events.ts new file mode 100644 index 0000000000000..89cf4d4602177 --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/upload_events.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { Client } from '@elastic/elasticsearch'; +import { chunk } from 'lodash'; +import pLimit from 'p-limit'; +import { inspect } from 'util'; +import { Fields } from '../../lib/entity'; +import { + ElasticsearchOutputWriteTargets, + toElasticsearchOutput, +} from '../../lib/output/to_elasticsearch_output'; +import { Logger } from './logger'; + +export function uploadEvents({ + events, + client, + workers, + writeTargets, + logger, +}: { + events: Fields[]; + client: Client; + workers: number; + writeTargets: ElasticsearchOutputWriteTargets; + logger: Logger; +}) { + const esDocuments = toElasticsearchOutput({ events, writeTargets }); + const fn = pLimit(workers); + + const batches = chunk(esDocuments, 5000); + + logger.debug(`Uploading ${esDocuments.length} in ${batches.length} batches`); + + const time = new Date().getTime(); + + return Promise.all( + batches.map((batch) => + fn(() => { + return client.bulk({ + require_alias: true, + body: batch.flatMap((doc) => { + return [{ index: { _index: doc._index } }, doc._source]; + }), + }); + }) + ) + ) + .then((results) => { + const errors = results + .flatMap((result) => result.body.items) + .filter((item) => !!item.index?.error) + .map((item) => item.index?.error); + + if (errors.length) { + // eslint-disable-next-line no-console + console.error(inspect(errors.slice(0, 10), { depth: null })); + throw new Error('Failed to upload some items'); + } + + logger.debug(`Uploaded ${events.length} in ${new Date().getTime() - time}ms`); + }) + .catch((err) => { + // eslint-disable-next-line no-console + console.error(err); + process.exit(1); + }); +} diff --git a/packages/elastic-apm-generator/src/test/scenarios/01_simple_trace.test.ts b/packages/elastic-apm-generator/src/test/scenarios/01_simple_trace.test.ts index 733093ce0a71c..866a9745befc3 100644 --- a/packages/elastic-apm-generator/src/test/scenarios/01_simple_trace.test.ts +++ b/packages/elastic-apm-generator/src/test/scenarios/01_simple_trace.test.ts @@ -18,7 +18,7 @@ describe('simple trace', () => { const range = timerange( new Date('2021-01-01T00:00:00.000Z').getTime(), - new Date('2021-01-01T00:15:00.000Z').getTime() - 1 + new Date('2021-01-01T00:15:00.000Z').getTime() ); events = range diff --git a/packages/elastic-apm-generator/src/test/scenarios/02_transaction_metrics.test.ts b/packages/elastic-apm-generator/src/test/scenarios/02_transaction_metrics.test.ts index 0b9f192d3d27d..58b28f71b9afc 100644 --- a/packages/elastic-apm-generator/src/test/scenarios/02_transaction_metrics.test.ts +++ b/packages/elastic-apm-generator/src/test/scenarios/02_transaction_metrics.test.ts @@ -19,7 +19,7 @@ describe('transaction metrics', () => { const range = timerange( new Date('2021-01-01T00:00:00.000Z').getTime(), - new Date('2021-01-01T00:15:00.000Z').getTime() - 1 + new Date('2021-01-01T00:15:00.000Z').getTime() ); events = getTransactionMetrics( diff --git a/packages/elastic-apm-generator/src/test/scenarios/03_span_destination_metrics.test.ts b/packages/elastic-apm-generator/src/test/scenarios/03_span_destination_metrics.test.ts index 158ccc5b5e714..0bf59f044bf03 100644 --- a/packages/elastic-apm-generator/src/test/scenarios/03_span_destination_metrics.test.ts +++ b/packages/elastic-apm-generator/src/test/scenarios/03_span_destination_metrics.test.ts @@ -19,7 +19,7 @@ describe('span destination metrics', () => { const range = timerange( new Date('2021-01-01T00:00:00.000Z').getTime(), - new Date('2021-01-01T00:15:00.000Z').getTime() - 1 + new Date('2021-01-01T00:15:00.000Z').getTime() ); events = getSpanDestinationMetrics( diff --git a/packages/elastic-apm-generator/src/test/scenarios/04_breakdown_metrics.test.ts b/packages/elastic-apm-generator/src/test/scenarios/04_breakdown_metrics.test.ts index aeb944f35faf6..469f56b99c5f2 100644 --- a/packages/elastic-apm-generator/src/test/scenarios/04_breakdown_metrics.test.ts +++ b/packages/elastic-apm-generator/src/test/scenarios/04_breakdown_metrics.test.ts @@ -26,7 +26,7 @@ describe('breakdown metrics', () => { const start = new Date('2021-01-01T00:00:00.000Z').getTime(); - const range = timerange(start, start + INTERVALS * 30 * 1000 - 1); + const range = timerange(start, start + INTERVALS * 30 * 1000); events = getBreakdownMetrics([ ...range diff --git a/packages/elastic-apm-generator/src/test/to_elasticsearch_output.test.ts b/packages/elastic-apm-generator/src/test/to_elasticsearch_output.test.ts index c1a5d47654fc9..d15ea89083112 100644 --- a/packages/elastic-apm-generator/src/test/to_elasticsearch_output.test.ts +++ b/packages/elastic-apm-generator/src/test/to_elasticsearch_output.test.ts @@ -9,6 +9,13 @@ import { Fields } from '../lib/entity'; import { toElasticsearchOutput } from '../lib/output/to_elasticsearch_output'; +const writeTargets = { + transaction: 'apm-8.0.0-transaction', + span: 'apm-8.0.0-span', + metric: 'apm-8.0.0-metric', + error: 'apm-8.0.0-error', +}; + describe('output to elasticsearch', () => { let event: Fields; @@ -21,13 +28,13 @@ describe('output to elasticsearch', () => { }); it('properly formats @timestamp', () => { - const doc = toElasticsearchOutput([event])[0] as any; + const doc = toElasticsearchOutput({ events: [event], writeTargets })[0] as any; expect(doc._source['@timestamp']).toEqual('2020-12-31T23:00:00.000Z'); }); it('formats a nested object', () => { - const doc = toElasticsearchOutput([event])[0] as any; + const doc = toElasticsearchOutput({ events: [event], writeTargets })[0] as any; expect(doc._source.processor).toEqual({ event: 'transaction', diff --git a/x-pack/test/apm_api_integration/common/trace_data.ts b/x-pack/test/apm_api_integration/common/trace_data.ts index 84bbb4beea4f4..9799e111cb135 100644 --- a/x-pack/test/apm_api_integration/common/trace_data.ts +++ b/x-pack/test/apm_api_integration/common/trace_data.ts @@ -20,15 +20,20 @@ export async function traceData(context: InheritedFtrProviderContext) { const es = context.getService('es'); return { index: (events: any[]) => { - const esEvents = toElasticsearchOutput( - [ + const esEvents = toElasticsearchOutput({ + events: [ ...events, ...getTransactionMetrics(events), ...getSpanDestinationMetrics(events), ...getBreakdownMetrics(events), ], - '7.14.0' - ); + writeTargets: { + transaction: 'apm-7.14.0-transaction', + span: 'apm-7.14.0-span', + error: 'apm-7.14.0-error', + metric: 'apm-7.14.0-metric', + }, + }); const batches = chunk(esEvents, 1000); const limiter = pLimit(1);