diff --git a/package.json b/package.json index 09af0dc9dcd94..6e60e258bb7de 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "@elastic/apm-generator": "link:bazel-bin/packages/elastic-apm-generator", "@elastic/apm-rum": "^5.9.1", "@elastic/apm-rum-react": "^1.3.1", - "@elastic/charts": "37.0.0", + "@elastic/charts": "38.0.1", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.21", "@elastic/ems-client": "7.16.0", @@ -765,6 +765,7 @@ "oboe": "^2.1.4", "parse-link-header": "^1.0.1", "pbf": "3.2.1", + "pdf-to-img": "^1.1.1", "pirates": "^4.0.1", "pixelmatch": "^5.1.0", "postcss": "^7.0.32", 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/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/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`, }, 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/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/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/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/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/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/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; diff --git a/src/plugins/vis_types/pie/server/plugin.ts b/src/plugins/vis_types/pie/server/plugin.ts index 48576bdff5d33..49b74e63b8c3c 100644 --- a/src/plugins/vis_types/pie/server/plugin.ts +++ b/src/plugins/vis_types/pie/server/plugin.ts @@ -14,8 +14,8 @@ import { CoreSetup, Plugin, UiSettingsParams } from 'kibana/server'; import { LEGACY_PIE_CHARTS_LIBRARY } from '../common'; export const getUiSettingsConfig: () => Record> = () => ({ - // TODO: Remove this when vis_type_vislib is removed - // https://github.com/elastic/kibana/issues/56143 + // TODO: Remove this when vislib pie is removed + // https://github.com/elastic/kibana/issues/111246 [LEGACY_PIE_CHARTS_LIBRARY]: { name: i18n.translate('visTypePie.advancedSettings.visualization.legacyPieChartsLibrary.name', { defaultMessage: 'Pie legacy charts library', @@ -33,7 +33,7 @@ export const getUiSettingsConfig: () => Record 'visTypePie.advancedSettings.visualization.legacyPieChartsLibrary.deprecation', { defaultMessage: - 'The legacy charts library for pie in visualize is deprecated and will not be supported as of 8.0.', + 'The legacy charts library for pie in visualize is deprecated and will not be supported in a future version.', } ), docLinksKey: 'visualizationSettings', diff --git a/src/plugins/vis_types/timeseries/public/application/components/vis_editor.tsx b/src/plugins/vis_types/timeseries/public/application/components/vis_editor.tsx index 9e46427e33c2e..caf7ac638af78 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/vis_editor.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/vis_editor.tsx @@ -79,6 +79,15 @@ export class VisEditor extends Component ({ + [TIME_RANGE_MODE_KEY]: + this.props.vis.title && + this.props.vis.params.type !== 'timeseries' && + val.override_index_pattern + ? TIME_RANGE_DATA_MODES.LAST_VALUE + : TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE, + ...val, + })), }, extractedIndexPatterns: [''], }; diff --git a/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/index.js b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/index.js index 6f6ddbbb7c414..2158283bb80d5 100644 --- a/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/index.js +++ b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/index.js @@ -144,7 +144,7 @@ export const TimeSeries = ({ debugState={window._echDebugStateFlag ?? false} showLegend={legend} showLegendExtra={true} - allowBrushingLastHistogramBucket={true} + allowBrushingLastHistogramBin={true} legendPosition={legendPosition} onBrushEnd={onBrushEndListener} onElementClick={(args) => handleElementClick(args)} diff --git a/src/plugins/vis_types/xy/public/components/xy_settings.tsx b/src/plugins/vis_types/xy/public/components/xy_settings.tsx index 74aff7535c2d8..304b0756c30b6 100644 --- a/src/plugins/vis_types/xy/public/components/xy_settings.tsx +++ b/src/plugins/vis_types/xy/public/components/xy_settings.tsx @@ -165,7 +165,7 @@ export const XYSettings: FC = ({ baseTheme={baseTheme} showLegend={showLegend} legendPosition={legendPosition} - allowBrushingLastHistogramBucket={isTimeChart} + allowBrushingLastHistogramBin={isTimeChart} roundHistogramBrushValues={enableHistogramMode && !isTimeChart} legendColorPicker={legendColorPicker} onElementClick={onElementClick} 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'); 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/apps/saved_objects_management/inspect_saved_objects.ts b/test/functional/apps/saved_objects_management/inspect_saved_objects.ts index 7fff2cc001844..2faa66d258eb6 100644 --- a/test/functional/apps/saved_objects_management/inspect_saved_objects.ts +++ b/test/functional/apps/saved_objects_management/inspect_saved_objects.ts @@ -20,20 +20,20 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const focusAndClickButton = async (buttonSubject: string) => { const button = await testSubjects.find(buttonSubject); await button.scrollIntoViewIfNecessary(); - await delay(10); + await delay(100); await button.focus(); - await delay(10); + await delay(100); await button.click(); // Allow some time for the transition/animations to occur before assuming the click is done - await delay(10); + await delay(100); }; + const textIncludesAll = (text: string, items: string[]) => { const bools = items.map((item) => !!text.includes(item)); return bools.every((currBool) => currBool === true); }; - // FLAKY: https://github.com/elastic/kibana/issues/68400 - describe.skip('saved objects edition page', () => { + describe('saved objects inspect page', () => { beforeEach(async () => { await esArchiver.load( 'test/functional/fixtures/es_archiver/saved_objects_management/edit_saved_object' @@ -74,13 +74,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.settings.clickKibanaSavedObjects(); let objects = await PageObjects.savedObjects.getRowTitles(); expect(objects.includes('A Dashboard')).to.be(true); + await PageObjects.savedObjects.clickInspectByTitle('A Dashboard'); await PageObjects.common.navigateToUrl('management', 'kibana/objects/dashboard/i-exist', { shouldUseHashForSubUrl: false, - }); + }); // we should wait for it to load. + // wait for the Inspect view to load + await PageObjects.savedObjects.waitInspectObjectIsLoaded(); await focusAndClickButton('savedObjectEditDelete'); await PageObjects.common.clickConfirmOnModal(); - objects = await PageObjects.savedObjects.getRowTitles(); expect(objects.includes('A Dashboard')).to.be(false); }); diff --git a/test/functional/apps/visualize/_area_chart.ts b/test/functional/apps/visualize/_area_chart.ts index 2a5be39403002..76bb1d2f58d05 100644 --- a/test/functional/apps/visualize/_area_chart.ts +++ b/test/functional/apps/visualize/_area_chart.ts @@ -95,11 +95,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show correct chart', async function () { const xAxisLabels = [ - '2015-09-19 12:00', - '2015-09-20 12:00', - '2015-09-21 12:00', - '2015-09-22 12:00', - '2015-09-23 12:00', + '2015-09-20 00:00', + '2015-09-21 00:00', + '2015-09-22 00:00', + '2015-09-23 00:00', ]; const yAxisLabels = ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400']; const expectedAreaChartData = [ diff --git a/test/functional/apps/visualize/_point_series_options.ts b/test/functional/apps/visualize/_point_series_options.ts index dbe26ba099590..a2d2831c87933 100644 --- a/test/functional/apps/visualize/_point_series_options.ts +++ b/test/functional/apps/visualize/_point_series_options.ts @@ -230,10 +230,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('timezones', async function () { it('should show round labels in default timezone', async function () { const expectedLabels = [ - '2015-09-19 12:00', - '2015-09-20 12:00', - '2015-09-21 12:00', - '2015-09-22 12:00', + '2015-09-20 00:00', + '2015-09-21 00:00', + '2015-09-22 00:00', + '2015-09-23 00:00', ]; await initChart(); const labels = await PageObjects.visChart.getXAxisLabels(xyChartSelector); @@ -242,11 +242,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show round labels in different timezone', async function () { const expectedLabels = [ - '2015-09-19 12:00', - '2015-09-20 12:00', - '2015-09-21 12:00', - '2015-09-22 12:00', - '2015-09-23 12:00', + '2015-09-20 00:00', + '2015-09-21 00:00', + '2015-09-22 00:00', + '2015-09-23 00:00', ]; await kibanaServer.uiSettings.update({ 'dateFormat:tz': 'America/Phoenix' }); 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/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() { diff --git a/test/functional/page_objects/management/saved_objects_page.ts b/test/functional/page_objects/management/saved_objects_page.ts index 9f48a6f57c8d8..21af7aa477abd 100644 --- a/test/functional/page_objects/management/saved_objects_page.ts +++ b/test/functional/page_objects/management/saved_objects_page.ts @@ -99,18 +99,30 @@ export class SavedObjectsPageObject extends FtrService { } async waitTableIsLoaded() { - return this.retry.try(async () => { + return await this.retry.try(async () => { const isLoaded = await this.find.existsByDisplayedByCssSelector( '*[data-test-subj="savedObjectsTable"] :not(.euiBasicTable-loading)' ); - if (isLoaded) { return true; } else { + this.log.debug(`still waiting for the table to load ${isLoaded}`); throw new Error('Waiting'); } }); } + async waitInspectObjectIsLoaded() { + return await this.retry.try(async () => { + this.log.debug(`wait for inspect view to load`); + const isLoaded = await this.find.byClassName('kibanaCodeEditor'); + const visibleContainerText = await isLoaded.getVisibleText(); + if (visibleContainerText) { + return true; + } else { + this.log.debug(`still waiting for json view to load ${isLoaded}`); + } + }); + } async clickRelationshipsByTitle(title: string) { const table = keyBy(await this.getElementsInTable(), 'title'); @@ -157,8 +169,10 @@ export class SavedObjectsPageObject extends FtrService { } async clickInspectByTitle(title: string) { + this.log.debug(`inspecting ${title} object through the context menu`); const table = keyBy(await this.getElementsInTable(), 'title'); if (table[title].menuElement) { + this.log.debug(`${title} has a menuElement`); await table[title].menuElement?.click(); // Wait for context menu to render const menuPanel = await this.find.byCssSelector('.euiContextMenuPanel'); @@ -166,6 +180,9 @@ export class SavedObjectsPageObject extends FtrService { await panelButton.click(); } else { // or the action elements are on the row without the menu + this.log.debug( + `${title} doesn't have a menu element, trying to copy the object instead using` + ); await table[title].copySaveObjectsElement?.click(); } } diff --git a/test/functional/services/lib/compare_pngs.ts b/test/functional/services/lib/compare_pngs.ts index fe1a1a359052b..521781c5a6d2b 100644 --- a/test/functional/services/lib/compare_pngs.ts +++ b/test/functional/services/lib/compare_pngs.ts @@ -10,26 +10,56 @@ import { parse, join } from 'path'; import Jimp from 'jimp'; import { ToolingLog } from '@kbn/dev-utils'; +interface PngDescriptor { + path: string; + + /** + * If a buffer is provided this will avoid the extra step of reading from disk + */ + buffer?: Buffer; +} + +const toDescriptor = (imageInfo: string | PngDescriptor): PngDescriptor => { + if (typeof imageInfo === 'string') { + return { path: imageInfo }; + } + return { + ...imageInfo, + }; +}; + +/** + * Override Jimp types that expect to be mapped to either string or buffer even though Jimp + * accepts both https://www.npmjs.com/package/jimp#basic-usage. + */ +const toJimp = (imageInfo: string | Buffer): Promise => { + return (Jimp.read as (value: string | Buffer) => Promise)(imageInfo); +}; + /** * Comparing pngs and writing result to provided directory * - * @param sessionPath - * @param baselinePath + * @param session + * @param baseline * @param diffPath * @param sessionDirectory * @param log * @returns Percent */ export async function comparePngs( - sessionPath: string, - baselinePath: string, + sessionInfo: string | PngDescriptor, + baselineInfo: string | PngDescriptor, diffPath: string, sessionDirectory: string, log: ToolingLog ) { - log.debug(`comparePngs: ${sessionPath} vs ${baselinePath}`); - const session = (await Jimp.read(sessionPath)).clone(); - const baseline = (await Jimp.read(baselinePath)).clone(); + const sessionDescriptor = toDescriptor(sessionInfo); + const baselineDescriptor = toDescriptor(baselineInfo); + + log.debug(`comparePngs: ${sessionDescriptor.path} vs ${baselineDescriptor.path}`); + + const session = (await toJimp(sessionDescriptor.buffer ?? sessionDescriptor.path)).clone(); + const baseline = (await toJimp(baselineDescriptor.buffer ?? baselineDescriptor.path)).clone(); if ( session.bitmap.width !== baseline.bitmap.width || @@ -63,8 +93,12 @@ export async function comparePngs( image.write(diffPath); // For debugging purposes it'll help to see the resized images and how they compare. - session.write(join(sessionDirectory, `${parse(sessionPath).name}-session-resized.png`)); - baseline.write(join(sessionDirectory, `${parse(baselinePath).name}-baseline-resized.png`)); + session.write( + join(sessionDirectory, `${parse(sessionDescriptor.path).name}-session-resized.png`) + ); + baseline.write( + join(sessionDirectory, `${parse(baselineDescriptor.path).name}-baseline-resized.png`) + ); } return percent; } diff --git a/x-pack/examples/reporting_example/common/index.ts b/x-pack/examples/reporting_example/common/index.ts index ba2fcd21c8c70..893bd4dee8ae1 100644 --- a/x-pack/examples/reporting_example/common/index.ts +++ b/x-pack/examples/reporting_example/common/index.ts @@ -8,6 +8,8 @@ export const PLUGIN_ID = 'reportingExample'; export const PLUGIN_NAME = 'reportingExample'; +export { MyForwardableState } from './types'; + export { REPORTING_EXAMPLE_LOCATOR_ID, ReportingExampleLocatorDefinition, diff --git a/x-pack/examples/reporting_example/common/locator.ts b/x-pack/examples/reporting_example/common/locator.ts index fc39ec1c52654..cbb7c7d110571 100644 --- a/x-pack/examples/reporting_example/common/locator.ts +++ b/x-pack/examples/reporting_example/common/locator.ts @@ -8,6 +8,7 @@ import { SerializableRecord } from '@kbn/utility-types'; import type { LocatorDefinition } from '../../../../src/plugins/share/public'; import { PLUGIN_ID } from '../common'; +import type { MyForwardableState } from '../public/types'; export const REPORTING_EXAMPLE_LOCATOR_ID = 'REPORTING_EXAMPLE_LOCATOR_ID'; @@ -20,10 +21,11 @@ export class ReportingExampleLocatorDefinition implements LocatorDefinition<{}> '1.0.0': (state: {}) => ({ ...state, migrated: true }), }; - public readonly getLocation = async (params: {}) => { + public readonly getLocation = async (params: MyForwardableState) => { + const path = Boolean(params.captureTest) ? '/captureTest' : '/'; return { app: PLUGIN_ID, - path: '/', + path, state: params, }; }; diff --git a/x-pack/examples/reporting_example/common/types.ts b/x-pack/examples/reporting_example/common/types.ts new file mode 100644 index 0000000000000..f05ba3a274525 --- /dev/null +++ b/x-pack/examples/reporting_example/common/types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Ensure, SerializableRecord } from '@kbn/utility-types'; + +export type MyForwardableState = Ensure< + SerializableRecord & { captureTest: 'A' }, + SerializableRecord +>; diff --git a/x-pack/examples/reporting_example/public/application.tsx b/x-pack/examples/reporting_example/public/application.tsx index d945048ecd73e..3e1afd7c517a2 100644 --- a/x-pack/examples/reporting_example/public/application.tsx +++ b/x-pack/examples/reporting_example/public/application.tsx @@ -7,23 +7,29 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { Router, Route, Switch } from 'react-router-dom'; import { AppMountParameters, CoreStart } from '../../../../src/core/public'; -import { ReportingExampleApp } from './components/app'; +import { CaptureTest } from './containers/capture_test'; +import { Main } from './containers/main'; +import { ApplicationContextProvider } from './application_context'; import { SetupDeps, StartDeps, MyForwardableState } from './types'; +import { ROUTES } from './constants'; export const renderApp = ( coreStart: CoreStart, deps: Omit, - { appBasePath, element }: AppMountParameters, // FIXME: appBasePath is deprecated + { appBasePath, element, history }: AppMountParameters, // FIXME: appBasePath is deprecated forwardedParams: MyForwardableState ) => { ReactDOM.render( - , + + + + } /> +
} /> + + + , element ); diff --git a/x-pack/examples/reporting_example/public/application_context.tsx b/x-pack/examples/reporting_example/public/application_context.tsx new file mode 100644 index 0000000000000..4ec16808f3f42 --- /dev/null +++ b/x-pack/examples/reporting_example/public/application_context.tsx @@ -0,0 +1,33 @@ +/* + * 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, { useContext, createContext, FC } from 'react'; + +import type { MyForwardableState } from './types'; + +interface ContextValue { + forwardedState?: MyForwardableState; +} + +const ApplicationContext = createContext(undefined); + +export const ApplicationContextProvider: FC<{ forwardedState: ContextValue['forwardedState'] }> = ({ + forwardedState, + children, +}) => { + return ( + {children} + ); +}; + +export const useApplicationContext = (): ContextValue => { + const ctx = useContext(ApplicationContext); + if (!ctx) { + throw new Error('useApplicationContext called outside of ApplicationContext!'); + } + return ctx; +}; diff --git a/x-pack/examples/reporting_example/public/components/index.ts b/x-pack/examples/reporting_example/public/components/index.ts new file mode 100644 index 0000000000000..7b138d90bb0a3 --- /dev/null +++ b/x-pack/examples/reporting_example/public/components/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 { TestImageA } from './test_image_a'; diff --git a/x-pack/examples/reporting_example/public/components/test_image_a.tsx b/x-pack/examples/reporting_example/public/components/test_image_a.tsx new file mode 100644 index 0000000000000..1ce94f35fdd29 --- /dev/null +++ b/x-pack/examples/reporting_example/public/components/test_image_a.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 type { FunctionComponent } from 'react'; +import React from 'react'; +import { VIS } from '../constants'; + +type Props = React.DetailedHTMLProps, HTMLImageElement>; + +export const TestImageA: FunctionComponent = ({ + width = VIS.width, + height = VIS.height, + ...restProps +}) => { + return ( + Test image + ); +}; + +const testImage = `iVBORw0KGgoAAAANSUhEUgAAB0gAAAKsCAYAAABmnO55AAAK32lDQ1BJQ0MgUHJvZmlsZQAASImVlwdUk8kWgOf/00NCgAACUkLvSCeAlNBDlw6iEpJAQgkhISCIDVlcwVVBRJqygKsiCq6ugKwFsWBFsYB9QRYF9blYsKHyfuARdved995595zJfOfmzp1775n5zx0AyH4soTANlgMgXZAlCvP1oMXExtFwI4AIKIAE8MCWxRYLGaGhgQCRufmv8r4fQNPzLfNpX//+/38VBQ5XzAYAikc4kSNmpyPchYznbKEoCwDUQUSvm5MlnOYbCCuKkAAR/n2ak2f54zQnzjCaNGMTEeaJMA0APInFEiUDQDJD9LRsdjLihzSdg6WAwxcgnI+wK5vH4iB8AmGz9PSMaR5F2AixFwJARqoD6Il/8pn8F/+JUv8sVrKUZ/OaEbwXXyxMY+X+n6X535KeJpnbwwAZJJ7IL2y6pkj97qZmBEhZkBgcMsd8zmzdp5kn8YucY7bYM26OOSyvAOnatODAOU7i+zClfrKYEXPMFXuHz7EoI0y6V5LIkzHHLNH8vpLUSKmex2VK/efxIqLnOJsfFTzH4tTwgHkbT6leJAmTxs8V+HrM7+sjzT1d/Kd8+Uzp2ixehJ80d9Z8/FwBY96nOEYaG4fr5T1vEym1F2Z5SPcSpoVK7blpvlK9ODtcujYLOZzza0OlNUxh+YfOMQgEvoAG/IAXCENmW4Bkn8VdmTWdiGeGMFfET+Zl0RjIbePSmAK2hRnN2tLaCoDpuzt7HN6GzdxJSPnUvC5jD3KM3yP3pXRel1gOQHsRACr353V6uwGgFALQ1s2WiLJndejpH8zMV0ERqAJNoAuMgDmwBvbAGbgDb+APQkAEiAXLARvwQDoQgRyQD9aDIlACtoEdoBrUgUawHxwCR0A7OAHOgAvgCrgB7oAHYBCMgBdgHLwHkxAE4SAyRIVUIS1IHzKFrCE65Ap5Q4FQGBQLJUDJkACSQPnQBqgEKoOqoXqoCfoZOg6dgS5BfdA9aAgag95An2EUTIIVYQ3YAF4E02EGHABHwMvgZDgTzoML4S1wJdwAH4Tb4DPwFfgOPAi/gCdQACWDUkZpo8xRdJQnKgQVh0pCiVBrUMWoClQDqgXViepB3UINol6iPqGxaCqahjZHO6P90JFoNjoTvQa9GV2N3o9uQ59D30IPocfR3zBkjDrGFOOEYWJiMMmYHEwRpgKzF3MMcx5zBzOCeY/FYpWxhlgHrB82FpuCXYXdjN2FbcV2Yfuww9gJHA6nijPFueBCcCxcFq4IV4U7iDuNu4kbwX3Ey+C18NZ4H3wcXoAvwFfgD+BP4W/in+EnCXIEfYITIYTAIeQSthL2EDoJ1wkjhEmiPNGQ6EKMIKYQ1xMriS3E88SHxLcyMjI6Mo4yS2T4MutkKmUOy1yUGZL5RFIgmZA8SfEkCWkLaR+pi3SP9JZMJhuQ3clx5CzyFnIT+Sz5MfmjLFXWQpYpy5FdK1sj2yZ7U/YVhUDRpzAoyyl5lArKUcp1yks5gpyBnKccS26NXI3ccbkBuQl5qryVfIh8uvxm+QPyl+RHFXAKBgreChyFQoVGhbMKw1QUVZfqSWVTN1D3UM9TRxSxioaKTMUUxRLFQ4q9iuNKCkq2SlFKK5VqlE4qDSqjlA2UmcppyluVjyj3K39eoLGAsYC7YNOClgU3F3xQWajirsJVKVZpVbmj8lmVpuqtmqpaqtqu+kgNrWaitkQtR2232nm1lwsVFzovZC8sXnhk4X11WN1EPUx9lXqj+lX1CQ1NDV8NoUaVxlmNl5rKmu6aKZrlmqc0x7SoWq5afK1yrdNaz2lKNAYtjVZJO0cb11bX9tOWaNdr92pP6hjqROoU6LTqPNIl6tJ1k3TLdbt1x/W09IL08vWa9e7rE/Tp+jz9nfo9+h8MDA2iDTYatBuMGqoYMg3zDJsNHxqRjdyMMo0ajG4bY43pxqnGu4xvmMAmdiY8kxqT66awqb0p33SXaZ8ZxszRTGDWYDZgTjJnmGebN5sPWShbBFoUWLRbvFqktyhuUeminkXfLO0s0yz3WD6wUrDytyqw6rR6Y21izbausb5tQ7bxsVlr02Hz2tbUlmu72/auHdUuyG6jXbfdV3sHe5F9i/2Yg55DgkOtwwBdkR5K30y/6Ihx9HBc63jC8ZOTvVOW0xGnP5zNnVOdDziPLjZczF28Z/Gwi44Ly6XeZdCV5prg+qProJu2G8utwe2Ju647x32v+zOGMSOFcZDxysPSQ+RxzOODp5Pnas8uL5SXr1exV6+3gnekd7X3Yx8dn2SfZp9xXzvfVb5dfhi/AL9SvwGmBpPNbGKO+zv4r/Y/F0AKCA+oDngSaBIoCuwMgoP8g7YHPQzWDxYEt4eAEGbI9pBHoYahmaG/LsEuCV1Ss+RpmFVYflhPODV8RfiB8PcRHhFbIx5EGkVKIrujKFHxUU1RH6K9osuiB2MWxayOuRKrFsuP7YjDxUXF7Y2bWOq9dMfSkXi7+KL4/mWGy1Yuu7RcbXna8pMrKCtYK44mYBKiEw4kfGGFsBpYE4nMxNrEcbYneyf7BcedU84Z47pwy7jPklySypJGk12StyeP8dx4FbyXfE9+Nf91il9KXcqH1JDUfalTadFpren49IT04wIFQargXIZmxsqMPqGpsEg4mOmUuSNzXBQg2iuGxMvEHVmKSJN0VWIk+U4ylO2aXZP9MScq5+hK+ZWClVdzTXI35T7L88n7aRV6FXtVd752/vr8odWM1fVroDWJa7rX6q4tXDuyznfd/vXE9anrrxVYFpQVvNsQvaGzUKNwXeHwd77fNRfJFomKBjY6b6z7Hv09//veTTabqjZ9K+YUXy6xLKko+bKZvfnyD1Y/VP4wtSVpS+9W+627t2G3Cbb1l7qV7i+TL8srG94etL2tnFZeXP5ux4odlypsK+p2EndKdg5WBlZ2VOlVbav6Us2rvlPjUdNaq167qfbDLs6um7vdd7fUadSV1H3+kf/j3Xrf+rYGg4aKRmxjduPTPVF7en6i/9S0V21vyd6v+wT7BveH7T/X5NDUdED9wNZmuFnSPHYw/uCNQ16HOlrMW+pblVtLDoPDksPPf074uf9IwJHuo/SjLb/o/1J7jHqsuA1qy20bb+e1D3bEdvQd9z/e3enceexXi1/3ndA+UXNS6eTWU8RThaemTuednugSdr08k3xmuHtF94OzMWdvn1tyrvd8wPmLF3wunO1h9Jy+6HLxxCWnS8cv0y+3X7G/0nbV7uqxa3bXjvXa97Zdd7jeccPxRmff4r5TN91unrnldevCbebtK3eC7/T1R/bfHYgfGLzLuTt6L+3e6/vZ9ycfrHuIeVj8SO5RxWP1xw2/Gf/WOmg/eHLIa+jqk/AnD4bZwy9+F//+ZaTwKflpxTOtZ02j1qMnxnzGbjxf+nzkhfDF5Muif8j/o/aV0atf/nD/4+p4zPjIa9HrqTeb36q+3ffO9l33ROjE4/fp7yc/FH9U/bj/E/1Tz+foz88mc77gvlR+Nf7a+S3g28Op9KkpIUvEmmkFUMiAk5IAeLMP6Y1jAaAifTlx6WxvPSPQ7HtghsB/4tn+e0bsAWgcACBiFQCB1wCoqkbaWcQ/BXkThFIQvTOAbWyk418iTrKxnvVFckNak0dTU2+NAMCVAvC1dGpqsnFq6msjEuwDALpyZ3v6adFE3hc5OIDuz+zvmECDv8lsv/+nHP8+g+kIbMHf538CjT8a9ZYUCwUAAACKZVhJZk1NACoAAAAIAAQBGgAFAAAAAQAAAD4BGwAFAAAAAQAAAEYBKAADAAAAAQACAACHaQAEAAAAAQAAAE4AAAAAAAAAkAAAAAEAAACQAAAAAQADkoYABwAAABIAAAB4oAIABAAAAAEAAAdIoAMABAAAAAEAAAKsAAAAAEFTQ0lJAAAAU2NyZWVuc2hvdH+sXfEAAAAJcEhZcwAAFiUAABYlAUlSJPAAAAHXaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA2LjAuMCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjY4NDwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4xODY0PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6VXNlckNvbW1lbnQ+U2NyZWVuc2hvdDwvZXhpZjpVc2VyQ29tbWVudD4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cj05w7IAAAAcaURPVAAAAAIAAAAAAAABVgAAACgAAAFWAAABVgAA0zwugxoiAABAAElEQVR4AezdB7yk8/XH8WN32WWxu5ZoQeRPoiwRJXpZPYiIGoII0cvqRK+rt43eExFliQS7ahBRo/feu0Qvyzb/OcNvnPnduXPnzjnrTvnM68Xze2bmOXfm/TzX9zf3eJ6Z7KvCTbghgAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACbSAwGQ3SNtjLvEUEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEECgK0CDlQEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgbYRoEHaNruaN4oAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAjRIOQYQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQKBtBGiQts2u5o0igAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAANUo4BBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBoG4G6G6QXXPhnGT36+opQe+65myz+s8UqPsadCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQE8J1N0gPeTQI+Scc8+v+LpHnHKirLfuOhUfa6U7v/jiC/nqq68qvqW+fftKr169Kj7Gnd+NwKuvvSYXX3yJTJgwUTb9zcbygx/M8d384Bp+yieffiqXXTZSnn76GVl//XVlicV/VsNWPf+UZn3dPS/HK0AAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIFGEZikDdJ99ztQ3n3n3dJ7PeWUE2SaqacurTf7YOVV15Cnnnq64tu489+3dtmQu/c/98lZZ51bcXu9c4q+U8iPfzS3DBkyvwxdYQXp06d3p89t1weu/NvfZdSo60pvf4ftt5VFF1242Lj++Zq/lMcee7z42I/mnltu+ef1Mtlkk5We25ODI448Ws448+zSS3jw/rtlxhlnlA8++ED22HPf0v0LLjhEdh22c2m9pwedve6efl38fAQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEECgVoGwBunZZ54mc8wxe/Hnfv/7s8rAgQNlqaVXkFdefbX0Wh575AGZbrpBpfVmH3gbpFdfc61sv8MuNTFog+/0006Reeedp6bnd/Wka68dLa+/8UbpaVv8djPp169fab1ZBscdf5KcfMofSy/33HPOkJ+vvpp8+eWX8sO55i3dr4MnH39IBgwYUHZfd1bGj58gF/7pzzJ+/PjiZnosb7jB+t0pUXrubzbdQm771+2l9b9cdEGhCb68vP32O7LIYkuW7tdLVf/tystK65Ny8M4778hVf7+69CMWGDJEll7629eiD3T2uksbMUAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEGlwgrEF66y03iDbx7K3VG6TrrLtB6QzSTz/9zL51qeUM0u40SLX4zDPPJP+86TpXky+9yNXXWLt0dqXe98RjDxab2unxZll21iDV13/a6WfK8KOOLb6VYbvsJHvvtbvrbX388ccy7/wLlWpos/rmG0eX1rszuOOOu2SrrbcVPW6WWnIJueSvFxXPEO7JBuk//3mrbL7FVqW3se02v5eDDtyvtK6Dzl532ZNYQQABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQaWIAGadDO0csJX3TRxaVq9TRIF/rJgrLiikOLNfTsztGjrys20EpFC4PjjztaNv71hvauusbt0CBVmP/+738yccKE4uVr64IyG0U2SLWsfofta6+/LnPPNVfppzR6g7Sz1116AwwQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgQYXaPgG6UsvvSwPPfxI4dKjb8sUU0whs8wyiyy91BJdnkWpl1h94IEH5eFHHi1c+ncOWXSRnxabZPfc+x957733i7uld+9esvJKK3X4bs/nnn9ennn6WXnzrbeK32U5a+Fn/nieH5U1svL9GtEg3WbrLeXggw4oldZLuq619q/KzvT87eabyvAjD5P77ntA3v3vf0vPHTJkPplj9q8vcZzufP31N+SRRx9LqzJL4QzUPn36yKuvvS4HH3KYvPXW26XHTjrxWOnff+rid57ONdf/le7XwcSJEwuWD8nzL7xQtJvnxz+Sn/50IRk8eLqy5+Ur+voeKfi/8cabxUbvzDPNWNwXCy+8kPTq1St/etV1fQ1PPvmU3H3PvYV9P60stNBPimcsVzuD9MabbpZx476+HK7u69VXW7XDz6h1X99627/kvf+9J8N227NUQ8/oPfSQg4rrSy25uAwaNEhefvkVeaLwOtNN759mmmnl/gcekP8UvnP2//7v/2TNNVaXBx98SN4qXE433ZYpXMpWL//bWYNUL397//0PFi9Z/dPCe19kkYWLvw9p+7Ts6j0//fQz8sKLL6WnF85eXVymnHJK+ectt8mjjz4qp552ZumxlVYaKhttuEFhX01WtNPvb+3sdZc2+mbQnd/b3Gy5ZZcumE0j/yt431PY36mJvNhii3T5e5+/DtYRQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgVygYRukn3z6qey40zDRy35Wuu2+2zDZY/dhlR6Ss84+Vw47fHiHx84641S5+JJL5fbb7yg99sB9d8tMhcad3l597TXZf/+D5ZZbbyuu5/9acegKcvRRR8iss86SPySTokGqP0QbVkcd/fVlYnV9qy23kMMOPUhOOnmEHH/CyXpX8bbl734rhx92cFotLtVALdJth+23lTFjxsgFF/453dVhuduuu8iee+xaul8baltvu4O8aJpq6UG9pPIF558tP/jBHOmu4nLs2LFy8KGHy5///O0ZtfYJut3w4YfJkkssbu/udHz33ffIdjvsXGyY2SftuMN2xYbvKSNOLd2dvoNU7/jxvAuUnYH7xmsvlp7X3X297PIrVTRIBS8tXCJ32UJjT88i1mMh3fR7Y88485xSk1svpzvy8r8W388114xKT5MbrrtGhgyZv2KDVPftttvvVHpuGhxy8AGy9e+3TKvFZbX3rE849rgTxXqdc/YZxf954KeLLFFWJ195/tknio1U3Q+VXnd6fj2/t7mZWl47arT85eJLUtnicvrpB8uIU06U5Zdbtux+VhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQACB7gg0ZINUm3ib/3YruavQGKt200aeNvTs7R9XXyM77Fi5carP0yaLnpmWbqlBqmdr/mq9DYtnyKXHKi1/+MM5ZdQ1V8m0005b9vCkaJB+9dVXsspqa5a+51R/oDZ511prDXn++Rdk+aGrlF6Dns14/3/uKq3rIG/qXTf6arn88itqbpC+8sqrsvY665V5lf2Awop6/uOqK8qapEcOP0ZOP+Os/Kkd1q8vvJ4FFhjS4X57h77PNX+xTlmj0z6e789aGqT17Ovc0r4GHXfWINWzel959dXS07vbIC1t2Mng7DNPkzXX/Hnp0Z5skNb7e5s3SJdeeim5887yY7n0BguD9Dtr72OMAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCNQq0JAN0i223Fpuuumfpfcw9dT95RdrrSl6dtq1144u3a+DYwpndG666SbF+/Synssst2LZ47rt4ov/TPSxSmdBalNRm4vnX/AnOfCgQ0vb6n165p7eLrn08rJtjz1muPxm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vHp+b7syo0GadFkigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBAhEBYg1RfTDqL7bQ/niIrr7yiLLX0ChUbNZVeuH6Hon6XYrpps+bCP11UPKvzrbfeTncXl3q51z1231WWXrr8UqTpSdok1e9EvPPuu+WRhx+Vnyy0oCyz1FLF73H83ZbbVGyQ6rZ6mVBt1px19rkdGlJ6Ft2vf72B/H6r30nv3r3TjyotJ1WDVN/rT36ygCyy8MKy5po/L/ve09IPLwyuv+FG2er329m7xH7/ZNkD36xcd/0NcvjhR5XtI/2+St0u3fTsVG2oXXb5yLKzP/Xx5ZZbRnbecXtZaqny/aANrauvGVVocJ8hTz31dCpVXGpjdJVVVpI/7LOXTFv43tJabu+++26xWT76uuslHQt6Vurxxx1V/B7Uww4fXirzpwvOLR57eseP512g9Jr12HzmqcdKz6t3X+t708b9aaefUaqtRfUSxb/dfFO55NLLRc/OTbejC83jzb5pHqf7dLnLsN3lyr99+926/7z5epnnxz+S/AxSvezygQf+QXbbba/Smcz6XvQs360Kx2L6rlxb+4MPPpADDzq0dOatPn+lFVeU7bbbWp5//oWyyzHbBqnW0G2POPJoufSykbak3HDdNTJkyPydvu705Hp+b7syyxukuu/vuvO29CNZIoAAAggggAACCCCAAAIIIIAAAggggAACCCCAAALdEghtkKafPOKUE2W9dddJq66lNkf++9//yvvvf1BsDmqDbcCAAVVr6qV233zrLZlpxhmlb9++pefqmYPaNLO3p554uEOjTps8eqbpO++8U/yZehlT+52fafsXXnix+L2oun5y4Ts5r712dHpI7vz3rcXvjCzdMYkH+fek6o977pnHZaqppuryJ2tT7L333pcBAwfI9IMHV2zCqunbBY8PCy5ac5ZZZi58N+WUXdZW87fffls+L5wJOmfhOzQ7+w7aLgt984Q33nizeAZt+s7PWrfr7Hm17ut8ez0u3333v4Uzlj+T731vBvf7yutXWtfG/5tvvVlwnFP69OnYpM+3GT9+grz19lsy80wz1/R8u336Hfpq4lfFM6z79etnH+5yXM/vbZdFeQICCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggECBQd4P00MOPlLPPPq/iS4hskFb8AZ3cqQ3KQw47onSWoV5S97hjjyo28saOHVs4M/Q8OfqY40pb6yVHH3mo/PtFSw/WMFh51TU6nCGZNvsuGqTa3Bt5xZXFBu8+++5fdtarXkr1iMMPSS+HJQIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIFATqbpDqGWZffjm2ImK/fn0LZ6z1qfjYpLxTz/hceNHyy73qz1tggSHy2GOPd/jRf9h3b9lpx/LL0nZ4UpU7erpBqpev1deQ37Txe8ftt3wnZzXmP5t1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBpZoO4GaaO+qVGjr5dttt2hy5e39167F5qj21f8PtEuN/7mCQcceIg8+eRTFZ9+2qmniF4OeFLe9Lswjzr62A4/4q8X/0mWX27ZDvdzBwIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAALtLtByDVLdoXpm5ZlnnSPX33Cj6Hdgptscs88uQ4bMJ7/73W9lySUWT3c37fLCP10kI0deWfy+1EGDBsqPfvwj2arw3oYMmb9p3xMvHAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIFJKdCSDVIL9v77H8gnn34i35thhuJ3kdrHGCOAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQHsJtHyDtL12J+8WAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0FICNEhbanfyZhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJoADdJqOjyGAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAItJUCDtKV2J28GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSqCdAgrabDYwgg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`; diff --git a/x-pack/examples/reporting_example/public/constants.ts b/x-pack/examples/reporting_example/public/constants.ts new file mode 100644 index 0000000000000..909b656c5e514 --- /dev/null +++ b/x-pack/examples/reporting_example/public/constants.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +// Values based on A4 page size +export const VIS = { + width: 1950 / 2, + height: 1200 / 2, +}; + +export const ROUTES = { + captureTest: '/captureTest', + main: '/', +}; diff --git a/x-pack/examples/reporting_example/public/containers/capture_test.scss b/x-pack/examples/reporting_example/public/containers/capture_test.scss new file mode 100644 index 0000000000000..4ecd869544b32 --- /dev/null +++ b/x-pack/examples/reporting_example/public/containers/capture_test.scss @@ -0,0 +1,10 @@ +.reportingExample { + &__captureContainer { + display: flex; + flex-direction: column; + align-items: center; + + margin-top: $euiSizeM; + margin-bottom: $euiSizeM; + } +} diff --git a/x-pack/examples/reporting_example/public/containers/capture_test.tsx b/x-pack/examples/reporting_example/public/containers/capture_test.tsx new file mode 100644 index 0000000000000..81528f8136dff --- /dev/null +++ b/x-pack/examples/reporting_example/public/containers/capture_test.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FunctionComponent } from 'react'; +import React from 'react'; +import { useHistory } from 'react-router-dom'; +import { parsePath } from 'history'; +import { + EuiTabbedContent, + EuiTabbedContentTab, + EuiSpacer, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiPage, + EuiPageHeader, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, +} from '@elastic/eui'; + +import { TestImageA } from '../components'; +import { useApplicationContext } from '../application_context'; +import { MyForwardableState } from '../types'; +import { ROUTES } from '../constants'; + +import './capture_test.scss'; + +const ItemsContainer: FunctionComponent<{ count: string }> = ({ count, children }) => ( +
+ {children} +
+); + +const tabs: Array = [ + { + id: 'A', + name: 'Test A', + content: ( + + + + + + + ), + }, +]; + +export const CaptureTest: FunctionComponent = () => { + const { forwardedState } = useApplicationContext(); + const tabToRender = forwardedState?.captureTest; + const history = useHistory(); + return ( + + + + + + + + Back to main + + + + + + + tab.id === tabToRender) : undefined + } + /> + + + + + ); +}; diff --git a/x-pack/examples/reporting_example/public/components/app.tsx b/x-pack/examples/reporting_example/public/containers/main.tsx similarity index 61% rename from x-pack/examples/reporting_example/public/components/app.tsx rename to x-pack/examples/reporting_example/public/containers/main.tsx index 3e2f08fc89c7b..8673c476fdc7b 100644 --- a/x-pack/examples/reporting_example/public/components/app.tsx +++ b/x-pack/examples/reporting_example/public/containers/main.tsx @@ -23,41 +23,42 @@ import { EuiTitle, EuiCodeBlock, EuiSpacer, + EuiLink, + EuiContextMenuProps, } from '@elastic/eui'; import moment from 'moment'; import { I18nProvider } from '@kbn/i18n/react'; import React, { useEffect, useState } from 'react'; -import { BrowserRouter as Router } from 'react-router-dom'; +import { parsePath } from 'history'; +import { BrowserRouter as Router, useHistory } from 'react-router-dom'; import * as Rx from 'rxjs'; import { takeWhile } from 'rxjs/operators'; import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/public'; -import { constants, ReportingStart } from '../../../../../x-pack/plugins/reporting/public'; +import { constants, ReportingStart } from '../../../../plugins/reporting/public'; import type { JobParamsPDFV2 } from '../../../../plugins/reporting/server/export_types/printable_pdf_v2/types'; import type { JobParamsPNGV2 } from '../../../../plugins/reporting/server/export_types/png_v2/types'; import { REPORTING_EXAMPLE_LOCATOR_ID } from '../../common'; -import { MyForwardableState } from '../types'; +import { useApplicationContext } from '../application_context'; +import { ROUTES } from '../constants'; +import type { MyForwardableState } from '../types'; interface ReportingExampleAppProps { basename: string; reporting: ReportingStart; screenshotMode: ScreenshotModePluginSetup; - forwardedParams?: MyForwardableState; } const sourceLogos = ['Beats', 'Cloud', 'Logging', 'Kibana']; -export const ReportingExampleApp = ({ - basename, - reporting, - screenshotMode, - forwardedParams, -}: ReportingExampleAppProps) => { +export const Main = ({ basename, reporting, screenshotMode }: ReportingExampleAppProps) => { + const history = useHistory(); + const { forwardedState } = useApplicationContext(); useEffect(() => { // eslint-disable-next-line no-console - console.log('forwardedParams', forwardedParams); - }, [forwardedParams]); + console.log('forwardedState', forwardedState); + }, [forwardedState]); // Context Menu const [isPopoverOpen, setPopover] = useState(false); @@ -123,12 +124,54 @@ export const ReportingExampleApp = ({ }; }; - const panels = [ + const getCaptureTestPNGJobParams = (): JobParamsPNGV2 => { + return { + version: '8.0.0', + layout: { + id: constants.LAYOUT_TYPES.PRESERVE_LAYOUT, + }, + locatorParams: { + id: REPORTING_EXAMPLE_LOCATOR_ID, + version: '0.5.0', + params: { captureTest: 'A' } as MyForwardableState, + }, + objectType: 'develeloperExample', + title: 'Reporting Developer Example', + browserTimezone: moment.tz.guess(), + }; + }; + + const getCaptureTestPDFJobParams = (print: boolean) => (): JobParamsPDFV2 => { + return { + version: '8.0.0', + layout: { + id: print ? constants.LAYOUT_TYPES.PRINT : constants.LAYOUT_TYPES.PRESERVE_LAYOUT, + dimensions: { + // Magic numbers based on height of components not rendered on this screen :( + height: 2400, + width: 1822, + }, + }, + locatorParams: [ + { + id: REPORTING_EXAMPLE_LOCATOR_ID, + version: '0.5.0', + params: { captureTest: 'A' } as MyForwardableState, + }, + ], + objectType: 'develeloperExample', + title: 'Reporting Developer Example', + browserTimezone: moment.tz.guess(), + }; + }; + + const panels: EuiContextMenuProps['panels'] = [ { id: 0, items: [ { name: 'PDF Reports', icon: 'document', panel: 1 }, { name: 'PNG Reports', icon: 'document', panel: 7 }, + { name: 'Capture test', icon: 'document', panel: 8, 'data-test-subj': 'captureTestPanel' }, ], }, { @@ -141,6 +184,31 @@ export const ReportingExampleApp = ({ { name: 'Canvas Layout Option', icon: 'canvasApp', panel: 3 }, ], }, + { + id: 8, + initialFocusedItemIndex: 0, + title: 'Capture test', + items: [ + { + name: 'Capture test A - PNG', + icon: 'document', + panel: 9, + 'data-test-subj': 'captureTestPNG', + }, + { + name: 'Capture test A - PDF', + icon: 'document', + panel: 10, + 'data-test-subj': 'captureTestPDF', + }, + { + name: 'Capture test A - PDF print optimized', + icon: 'document', + panel: 11, + 'data-test-subj': 'captureTestPDFPrint', + }, + ], + }, { id: 7, initialFocusedItemIndex: 0, @@ -188,6 +256,37 @@ export const ReportingExampleApp = ({ /> ), }, + { + id: 9, + title: 'Test A', + content: ( + + ), + }, + { + id: 10, + title: 'Test A', + content: ( + + ), + }, + { + id: 11, + title: 'Test A', + content: ( + + ), + }, ]; return ( @@ -207,30 +306,45 @@ export const ReportingExampleApp = ({ - Share} - isOpen={isPopoverOpen} - closePopover={closePopover} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - + + + + Share + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + + + + + + Go to capture test + + + +
- {forwardedParams ? ( + {forwardedState ? ( <>

Forwarded app state

- {JSON.stringify(forwardedParams)} + {JSON.stringify(forwardedState)} ) : ( <> diff --git a/x-pack/examples/reporting_example/public/types.ts b/x-pack/examples/reporting_example/public/types.ts index fb28293ab63a3..732de505acf76 100644 --- a/x-pack/examples/reporting_example/public/types.ts +++ b/x-pack/examples/reporting_example/public/types.ts @@ -10,6 +10,7 @@ import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/public'; import { SharePluginSetup } from 'src/plugins/share/public'; import { DeveloperExamplesSetup } from '../../../../examples/developer_examples/public'; import { ReportingStart } from '../../../plugins/reporting/public'; +import type { MyForwardableState } from '../common'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PluginSetup {} @@ -26,4 +27,4 @@ export interface StartDeps { reporting: ReportingStart; } -export type MyForwardableState = Record; +export type { MyForwardableState }; 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/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/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/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/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({ } 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/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/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..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 @@ -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'; @@ -24,15 +24,14 @@ 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 = { - dataLoading: false, queries: ['query A', 'query B'], isFlyoutOpen: false, curation: { @@ -97,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', () => { @@ -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..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 @@ -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'; @@ -25,17 +25,17 @@ 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 }>(); 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,12 +96,11 @@ export const AutomatedCuration: React.FC = () => { ], tabs: pageTabs, }} - isLoading={dataLoading} > {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 ( 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' && } 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] - ); -}; 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( 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 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 () => { 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 = 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/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/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/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap index 0fad522624975..b058c42d8b4d1 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap @@ -5,7 +5,7 @@ exports[`xy_expression XYChart component it renders area 1`] = ` renderer="canvas" > { expect(wrapper.find(Settings).first().prop('onBrushEnd')).toBeUndefined(); }); - test('allowBrushingLastHistogramBucket is true for date histogram data', () => { + test('allowBrushingLastHistogramBin is true for date histogram data', () => { const { args } = sampleArgs(); const wrapper = mountWithIntl( @@ -1182,7 +1182,7 @@ describe('xy_expression', () => { }} /> ); - expect(wrapper.find(Settings).at(0).prop('allowBrushingLastHistogramBucket')).toEqual(true); + expect(wrapper.find(Settings).at(0).prop('allowBrushingLastHistogramBin')).toEqual(true); }); test('onElementClick returns correct context data', () => { @@ -1445,7 +1445,7 @@ describe('xy_expression', () => { }); }); - test('allowBrushingLastHistogramBucket should be fakse for ordinal data', () => { + test('allowBrushingLastHistogramBin should be fakse for ordinal data', () => { const { args, data } = sampleArgs(); const wrapper = mountWithIntl( @@ -1472,7 +1472,7 @@ describe('xy_expression', () => { /> ); - expect(wrapper.find(Settings).at(0).prop('allowBrushingLastHistogramBucket')).toEqual(false); + expect(wrapper.find(Settings).at(0).prop('allowBrushingLastHistogramBin')).toEqual(false); }); test('onElementClick is not triggering event on non-interactive mode', () => { @@ -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 36f1b92b8a1f4..32ca4c982c10e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -594,18 +594,22 @@ export function XYChart({ boundary: document.getElementById('app-fixed-viewport') ?? undefined, headerFormatter: (d) => safeXAccessorLabelRenderer(d.value), }} - allowBrushingLastHistogramBucket={Boolean(isTimeViz)} + allowBrushingLastHistogramBin={Boolean(isTimeViz)} rotation={shouldRotate ? 90 : 0} 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} /> 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, }, ], 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; } 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: 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/plugins/reporting/public/share_context_menu/__snapshots__/screen_capture_panel_content.test.tsx.snap b/x-pack/plugins/reporting/public/share_context_menu/__snapshots__/screen_capture_panel_content.test.tsx.snap index e655e9c3d7f06..c5d87410f596f 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/__snapshots__/screen_capture_panel_content.test.tsx.snap +++ b/x-pack/plugins/reporting/public/share_context_menu/__snapshots__/screen_capture_panel_content.test.tsx.snap @@ -346,88 +346,88 @@ exports[`ScreenCapturePanelContent properly renders a view with "canvas" layout className="euiSpacer euiSpacer--s" /> - - + - - + + + Unsaved work + +
+ - - - - - - - +

+ + + Save your work before copying this URL. + + +

+
+ + +
+ +
+ +
+
+
+ + @@ -784,88 +784,88 @@ exports[`ScreenCapturePanelContent properly renders a view with "print" layout o className="euiSpacer euiSpacer--s" />
- - + - - + + + Unsaved work + + + - - - - - - - +

+ + + Save your work before copying this URL. + + +

+ + + +
+ +
+ + + + + + @@ -1094,88 +1094,88 @@ exports[`ScreenCapturePanelContent renders the default view properly 1`] = ` className="euiSpacer euiSpacer--s" />
- - + - - - - - - - +

+ + + Save your work before copying this URL. + + +

+ + + +
+ +
+ + + + + + diff --git a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/components/error_unsaved_work_panel.tsx b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/components/error_unsaved_work_panel.tsx new file mode 100644 index 0000000000000..348c6d42cddb8 --- /dev/null +++ b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/components/error_unsaved_work_panel.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { i18n } from '@kbn/i18n'; +import type { FunctionComponent } from 'react'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCallOut, EuiText, EuiSpacer } from '@elastic/eui'; + +const i18nTexts = { + title: i18n.translate('xpack.reporting.panelContent.unsavedStateErrorTitle', { + defaultMessage: 'Unsaved work', + }), +}; + +export const ErrorUnsavedWorkPanel: FunctionComponent = () => { + return ( + + +

+ +

+
+ +
+ ); +}; diff --git a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/components/error_url_too_long_panel.tsx b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/components/error_url_too_long_panel.tsx new file mode 100644 index 0000000000000..9c925fe03fee2 --- /dev/null +++ b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/components/error_url_too_long_panel.tsx @@ -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 type { FunctionComponent } from 'react'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCallOut, EuiText } from '@elastic/eui'; + +interface Props { + isUnsaved?: boolean; +} + +const i18nTexts = { + title: i18n.translate('xpack.reporting.panelContent.unsavedStateAndExceedsMaxLengthTitle', { + defaultMessage: 'URL too long', + }), +}; + +export const ErrorUrlTooLongPanel: FunctionComponent = ({ isUnsaved }) => ( + + +

+ {isUnsaved ? ( + + + + ) : ( + // Reaching this state is essentially just an error and should result in a user contacting us. + + + + )} +

+
+
+); diff --git a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/components/index.ts b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/components/index.ts new file mode 100644 index 0000000000000..b7da9d6d12573 --- /dev/null +++ b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/components/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { ErrorUnsavedWorkPanel } from './error_unsaved_work_panel'; +export { ErrorUrlTooLongPanel } from './error_url_too_long_panel'; diff --git a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/constants.ts b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/constants.ts new file mode 100644 index 0000000000000..fe7faf48d6106 --- /dev/null +++ b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/constants.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Based on {@link URL_MAX_LENGTH} exported from core/public. + */ +const CHROMIUM_MAX_URL_LENGTH = 25 * 1000; + +export const getMaxUrlLength = () => CHROMIUM_MAX_URL_LENGTH; diff --git a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/index.ts b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/index.ts new file mode 100644 index 0000000000000..843a0b6747e4c --- /dev/null +++ b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/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 { ReportingPanelContent, Props, ReportingPanelProps } from './reporting_panel_content'; diff --git a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.test.tsx b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.test.tsx similarity index 67% rename from x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.test.tsx rename to x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.test.tsx index 6ad894bf3ac2f..e9dd584e51f82 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.test.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.test.tsx @@ -12,8 +12,14 @@ import { notificationServiceMock, uiSettingsServiceMock, } from 'src/core/public/mocks'; -import { ReportingAPIClient } from '../lib/reporting_api_client'; -import { ReportingPanelContent, ReportingPanelProps as Props } from './reporting_panel_content'; +import { ReportingAPIClient } from '../../lib/reporting_api_client'; +import { ReportingPanelContent, ReportingPanelProps as Props } from '.'; +import { ErrorUnsavedWorkPanel } from './components'; + +jest.mock('./constants', () => ({ + getMaxUrlLength: jest.fn(() => 9999999), +})); +import * as constants from './constants'; describe('ReportingPanelContent', () => { const props: Partial = { @@ -83,7 +89,7 @@ describe('ReportingPanelContent', () => { }); it('changing the layout triggers refreshing the state with the latest job params', () => { - const wrapper = mountComponent({ requiresSavedState: false }); + const wrapper = mountComponent({ requiresSavedState: false, isDirty: false }); wrapper.update(); expect(wrapper.find('EuiCopy').prop('textToCopy')).toMatchInlineSnapshot( `"http://localhost/api/reporting/generate/test?jobParams=%28appState%3Avery_cool_app_state_X%2CbrowserTimezone%3AMars%2CobjectType%3Anoice_object%2Ctitle%3Aultimate_title%2Cversion%3A%277.15.0-test%27%29"` @@ -97,4 +103,35 @@ describe('ReportingPanelContent', () => { ); }); }); + + describe('copy post URL', () => { + it('shows the copy button without warnings', () => { + const wrapper = mountComponent({ requiresSavedState: false, isDirty: false }); + wrapper.update(); + expect(wrapper.exists('EuiCopy')).toBe(true); + expect(wrapper.exists(ErrorUnsavedWorkPanel)).toBe(false); + }); + + it('does not show the copy button when there is unsaved state', () => { + const wrapper = mountComponent({ requiresSavedState: false, isDirty: true }); + wrapper.update(); + expect(wrapper.exists('EuiCopy')).toBe(false); + expect(wrapper.exists(ErrorUnsavedWorkPanel)).toBe(true); + }); + + it('does not show the copy button when the URL is too long', () => { + (constants.getMaxUrlLength as jest.Mock).mockReturnValue(1); + const wrapper = mountComponent({ requiresSavedState: false, isDirty: true }); + wrapper.update(); + + expect(wrapper.exists('EuiCopy')).toBe(false); + expect(wrapper.exists('[data-test-subj="urlTooLongTrySavingMessage"]')).toBe(true); + expect(wrapper.exists('[data-test-subj="urlTooLongErrorMessage"]')).toBe(false); + + wrapper.setProps({ isDirty: false }); + expect(wrapper.exists('EuiCopy')).toBe(false); + expect(wrapper.exists('[data-test-subj="urlTooLongTrySavingMessage"]')).toBe(false); + expect(wrapper.exists('[data-test-subj="urlTooLongErrorMessage"]')).toBe(true); + }); + }); }); diff --git a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.tsx similarity index 84% rename from x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx rename to x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.tsx index 6ed6f2d0c5f49..4e05dc5637bfb 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.tsx @@ -20,16 +20,18 @@ import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component, ReactElement } from 'react'; import { IUiSettingsClient, ToastsSetup } from 'src/core/public'; import url from 'url'; -import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; +import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; import { CSV_REPORT_TYPE, PDF_REPORT_TYPE, PDF_REPORT_TYPE_V2, PNG_REPORT_TYPE, PNG_REPORT_TYPE_V2, -} from '../../common/constants'; -import { BaseParams } from '../../common/types'; -import { ReportingAPIClient } from '../lib/reporting_api_client'; +} from '../../../common/constants'; +import { BaseParams } from '../../../common/types'; +import { ReportingAPIClient } from '../../lib/reporting_api_client'; +import { ErrorUnsavedWorkPanel, ErrorUrlTooLongPanel } from './components'; +import { getMaxUrlLength } from './constants'; export interface ReportingPanelProps { apiClient: ReportingAPIClient; @@ -108,11 +110,39 @@ class ReportingPanelContentUi extends Component { return this.props.objectId === undefined || this.props.objectId === ''; }; + private renderCopyURLButton({ + isUnsaved, + exceedsMaxLength, + }: { + isUnsaved: boolean; + exceedsMaxLength: boolean; + }) { + if (isUnsaved) { + if (exceedsMaxLength) { + return ; + } + return ; + } else if (exceedsMaxLength) { + return ; + } + return ( + + {(copy) => ( + + + + )} + + ); + } + public render() { - if ( - this.props.requiresSavedState && - (this.isNotSaved() || this.props.isDirty || this.state.isStale) - ) { + const isUnsaved: boolean = this.isNotSaved() || this.props.isDirty || this.state.isStale; + + if (this.props.requiresSavedState && isUnsaved) { return ( { ); } + const exceedsMaxLength = this.state.absoluteUrl.length >= getMaxUrlLength(); + return ( @@ -172,17 +204,7 @@ class ReportingPanelContentUi extends Component {

- - - {(copy) => ( - - - - )} - + {this.renderCopyURLButton({ isUnsaved, exceedsMaxLength })}
); diff --git a/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.test.tsx b/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.test.tsx index b64052228eb41..f37aaea114cfa 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.test.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.test.tsx @@ -85,8 +85,10 @@ test('ScreenCapturePanelContent decorated job params are visible in the POST URL const component = mount( -> = function createJobFactoryFn(_reporting, logger) { +> = function createJobFactoryFn(reporting, logger) { return async function createJob(jobParams, context) { logger.warn( - `The "/generate/csv" endpoint is deprecated and will be removed in Kibana 8.0. Please recreate the POST URL used to automate this CSV export.` + `The "/generate/csv" endpoint is deprecated. Please recreate the POST URL used to automate this CSV export.` ); - const savedObjectsClient = context.core.savedObjects.client; - const indexPatternSavedObject = (await savedObjectsClient.get( - 'index-pattern', - jobParams.indexPatternId - )) as unknown as IndexPatternSavedObjectDeprecatedCSV; - return { isDeprecated: true, - indexPatternSavedObject, ...jobParams, }; }; diff --git a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts index cb103812c7f2a..57f030df66e0e 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts @@ -12,6 +12,7 @@ import { ElasticsearchClient, IUiSettingsClient } from 'kibana/server'; import moment from 'moment'; import Puid from 'puid'; import sinon from 'sinon'; +import type { DataView, DataViewsService } from 'src/plugins/data/common'; import { ReportingConfig, ReportingCore } from '../../'; import { FieldFormatsRegistry, @@ -56,6 +57,8 @@ describe('CSV Execute Job', function () { let encryptedHeaders: any; let configGetStub: any; + let mockDataView: jest.Mocked; + let mockDataViewsService: jest.Mocked; let mockEsClient: DeeplyMockedKeys; let mockReportingConfig: ReportingConfig; let mockReportingCore: ReportingCore; @@ -81,10 +84,15 @@ describe('CSV Execute Job', function () { configGetStub.withArgs('csv', 'maxSizeBytes').returns(1024 * 1000); // 1mB configGetStub.withArgs('csv', 'scroll').returns({}); mockReportingConfig = { get: configGetStub, kbnConfig: { get: configGetStub } }; + mockDataView = { fieldFormatMap: {}, fields: [] } as unknown as typeof mockDataView; + mockDataViewsService = { + get: jest.fn().mockResolvedValue(mockDataView), + } as unknown as typeof mockDataViewsService; mockReportingCore = await createMockReportingCore(createMockConfigSchema()); mockReportingCore.getUiSettingsServiceFactory = () => Promise.resolve(mockUiSettingsClient as unknown as IUiSettingsClient); + mockReportingCore.getDataViewsService = jest.fn().mockResolvedValue(mockDataViewsService); mockReportingCore.setConfig(mockReportingConfig); mockEsClient = (await mockReportingCore.getEsClient()).asScoped({} as any) @@ -931,16 +939,14 @@ describe('CSV Execute Job', function () { fields: ['one', 'two'], conflictedTypesFields: [], searchRequest: { index: null, body: null }, - indexPatternSavedObject: { - id: 'logstash-*', - type: 'index-pattern', - attributes: { - title: 'logstash-*', - fields: '[{"name":"one","type":"string"}, {"name":"two","type":"string"}]', - fieldFormatMap: '{"one":{"id":"string","params":{"transform": "upper"}}}', - }, - }, + indexPatternId: 'something', }); + + mockDataView.fieldFormatMap = { one: { id: 'string', params: { transform: 'upper' } } }; + mockDataView.fields = [ + { name: 'one', type: 'string' }, + { name: 'two', type: 'string' }, + ] as typeof mockDataView.fields; await runTask('job123', jobParams, cancellationToken, stream); expect(content).not.toBe(null); const lines = content!.split('\n'); diff --git a/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts index 9ce0bb6d6de00..a007591821988 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts @@ -25,12 +25,14 @@ export const runTaskFnFactory: RunTaskFnFactory = {}; configMock[FORMATS_UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP] = { number: { id: 'number', params: {} }, @@ -39,13 +38,9 @@ describe('field format map', function () { const fieldFormatsRegistry = new FieldFormatsRegistry(); fieldFormatsRegistry.init(getConfig, {}, [BytesFormat, NumberFormat]); - const formatMap = fieldFormatMapFactory( - indexPatternSavedObject, - fieldFormatsRegistry, - mockTimezone - ); + const formatMap = fieldFormatMapFactory(dataView, fieldFormatsRegistry, mockTimezone); - it('should build field format map with entry per index pattern field', function () { + it('should build field format map with entry per data view field', function () { expect(formatMap.has('field1')).to.be(true); expect(formatMap.has('field2')).to.be(true); expect(formatMap.has('field_not_in_index')).to.be(false); diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/field_format_map.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/field_format_map.ts index 9d094f4308ed7..38a6cac337861 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/field_format_map.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/field_format_map.ts @@ -6,22 +6,21 @@ */ import _ from 'lodash'; +import type { DataView, KBN_FIELD_TYPES } from 'src/plugins/data/common'; import { FieldFormat, IFieldFormatsRegistry, FieldFormatConfig, } from 'src/plugins/field_formats/common'; -import { IndexPatternSavedObjectDeprecatedCSV } from '../types'; - /** * Create a map of FieldFormat instances for index pattern fields * - * @param {Object} indexPatternSavedObject + * @param {DataView} dataView * @param {FieldFormatsService} fieldFormats * @return {Map} key: field name, value: FieldFormat instance */ export function fieldFormatMapFactory( - indexPatternSavedObject: IndexPatternSavedObjectDeprecatedCSV, + dataView: DataView | undefined, fieldFormatsRegistry: IFieldFormatsRegistry, timezone: string | undefined ) { @@ -32,10 +31,9 @@ export function fieldFormatMapFactory( const serverDateParams = { timezone }; // Add FieldFormat instances for fields with custom formatters - if (_.has(indexPatternSavedObject, 'attributes.fieldFormatMap')) { - const fieldFormatMap = JSON.parse(indexPatternSavedObject.attributes.fieldFormatMap); - Object.keys(fieldFormatMap).forEach((fieldName) => { - const formatConfig: FieldFormatConfig = fieldFormatMap[fieldName]; + if (dataView) { + Object.keys(dataView.fieldFormatMap).forEach((fieldName) => { + const formatConfig: FieldFormatConfig = dataView.fieldFormatMap[fieldName]; const formatParams = { ...formatConfig.params, ...serverDateParams, @@ -48,12 +46,11 @@ export function fieldFormatMapFactory( } // Add default FieldFormat instances for non-custom formatted fields - const indexFields = JSON.parse(_.get(indexPatternSavedObject, 'attributes.fields', '[]')); - indexFields.forEach((field: any) => { + dataView?.fields.forEach((field) => { if (!formatsMap.has(field.name)) { formatsMap.set( field.name, - fieldFormatsRegistry.getDefaultInstance(field.type, [], serverDateParams) + fieldFormatsRegistry.getDefaultInstance(field.type as KBN_FIELD_TYPES, [], serverDateParams) ); } }); diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts index 61f404ed2fb02..2573ba14f22a5 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts @@ -8,6 +8,7 @@ import { Writable } from 'stream'; import { i18n } from '@kbn/i18n'; import { ElasticsearchClient, IUiSettingsClient } from 'src/core/server'; +import type { DataView, DataViewsService } from 'src/plugins/data/common'; import { ReportingConfig } from '../../../'; import { createEscapeValue } from '../../../../../../../src/plugins/data/common'; import { CancellationToken } from '../../../../../../plugins/reporting/common'; @@ -16,10 +17,7 @@ import { byteSizeValueToNumber } from '../../../../common/schema_utils'; import { LevelLogger } from '../../../lib'; import { getFieldFormats } from '../../../services'; import { MaxSizeStringBuilder } from '../../csv_searchsource/generate_csv/max_size_string_builder'; -import { - IndexPatternSavedObjectDeprecatedCSV, - SavedSearchGeneratorResultDeprecatedCSV, -} from '../types'; +import { SavedSearchGeneratorResultDeprecatedCSV } from '../types'; import { checkIfRowsHaveFormulas } from './check_cells_for_formulas'; import { fieldFormatMapFactory } from './field_format_map'; import { createFlattenHit } from './flatten_hit'; @@ -44,7 +42,7 @@ interface SearchRequest { export interface GenerateCsvParams { browserTimezone?: string; searchRequest: SearchRequest; - indexPatternSavedObject: IndexPatternSavedObjectDeprecatedCSV; + indexPatternId: string; fields: string[]; metaFields: string[]; conflictedTypesFields: string[]; @@ -58,6 +56,7 @@ export function createGenerateCsv(logger: LevelLogger) { config: ReportingConfig, uiSettingsClient: IUiSettingsClient, elasticsearchClient: ElasticsearchClient, + dataViews: DataViewsService, cancellationToken: CancellationToken, stream: Writable ): Promise { @@ -91,11 +90,17 @@ export function createGenerateCsv(logger: LevelLogger) { let csvContainsFormulas = false; const flattenHit = createFlattenHit(fields, metaFields, conflictedTypesFields); + let dataView: DataView | undefined; + + try { + dataView = await dataViews.get(job.indexPatternId); + } catch (error) { + logger.error(`Failed to get the data view "${job.indexPatternId}": ${error}`); + } + const formatsMap = await getFieldFormats() .fieldFormatServiceFactory(uiSettingsClient) - .then((fieldFormats) => - fieldFormatMapFactory(job.indexPatternSavedObject, fieldFormats, settings.timezone) - ); + .then((fieldFormats) => fieldFormatMapFactory(dataView, fieldFormats, settings.timezone)); const formatCsvValues = createFormatCsvValues( escapeValue, diff --git a/x-pack/plugins/reporting/server/export_types/csv/types.d.ts b/x-pack/plugins/reporting/server/export_types/csv/types.d.ts index 0263e82040f17..fff6f0bcf9538 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/types.d.ts @@ -5,20 +5,11 @@ * 2.0. */ +import type { FieldSpec } from 'src/plugins/data/common'; import { BaseParams, BasePayload } from '../../types'; export type RawValue = string | object | null | undefined; -export interface IndexPatternSavedObjectDeprecatedCSV { - title: string; - timeFieldName: string; - fields?: any[]; - attributes: { - fields: string; - fieldFormatMap: string; - }; -} - interface BaseParamsDeprecatedCSV { searchRequest: SearchRequestDeprecatedCSV; fields: string[]; @@ -31,10 +22,9 @@ export type JobParamsDeprecatedCSV = BaseParamsDeprecatedCSV & indexPatternId: string; }; -// CSV create job method converts indexPatternID to indexPatternSavedObject export type TaskPayloadDeprecatedCSV = BaseParamsDeprecatedCSV & BasePayload & { - indexPatternSavedObject: IndexPatternSavedObjectDeprecatedCSV; + indexPatternId: string; }; export interface SearchRequestDeprecatedCSV { diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.test.ts index e2c3ffdd68818..0166e82744e5d 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.test.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.test.ts @@ -48,11 +48,11 @@ test(`passes title through if provided`, async () => { test(`gets the title from the savedObject`, async () => { const createJobMock = jest.fn(); const title = 'savedTitle'; - mockRequestHandlerContext.core.savedObjects.client.get.mockResolvedValue( - createMockSavedObject({ + mockRequestHandlerContext.core.savedObjects.client.resolve.mockResolvedValue({ + saved_object: createMockSavedObject({ attributes: { title }, - }) - ); + }), + } as any); await compatibilityShim(createJobMock, mockLogger)( createMockJobParams({ objectType: 'search', savedObjectId: 'abc' }), @@ -72,9 +72,9 @@ test(`gets the title from the savedObject`, async () => { test(`passes the objectType and savedObjectId to the savedObjectsClient`, async () => { const createJobMock = jest.fn(); const context = mockRequestHandlerContext; - context.core.savedObjects.client.get.mockResolvedValue( - createMockSavedObject({ attributes: { title: '' } }) - ); + context.core.savedObjects.client.resolve.mockResolvedValue({ + saved_object: createMockSavedObject({ attributes: { title: '' } }), + } as any); const objectType = 'search'; const savedObjectId = 'abc'; @@ -92,10 +92,8 @@ test(`passes the objectType and savedObjectId to the savedObjectsClient`, async ); expect(mockLogger.error.mock.calls.length).toBe(0); - const getMock = context.core.savedObjects.client.get.mock; - expect(getMock.calls.length).toBe(1); - expect(getMock.calls[0][0]).toBe(objectType); - expect(getMock.calls[0][1]).toBe(savedObjectId); + expect(context.core.savedObjects.client.resolve).toHaveBeenCalledTimes(1); + expect(context.core.savedObjects.client.resolve).toHaveBeenCalledWith(objectType, savedObjectId); }); test(`logs no warnings when title and relativeUrls is passed`, async () => { diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.ts index 1d222d61eb07d..d5e78bcf68f4b 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.ts @@ -22,7 +22,10 @@ const getSavedObjectTitle = async ( savedObjectId: string, savedObjectsClient: SavedObjectsClientContract ) => { - const savedObject = await savedObjectsClient.get<{ title: string }>(objectType, savedObjectId); + const { saved_object: savedObject } = await savedObjectsClient.resolve<{ title: string }>( + objectType, + savedObjectId + ); return savedObject.attributes.title; }; diff --git a/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts b/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts index 622097f8dbd32..5900a151f92da 100644 --- a/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts +++ b/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts @@ -71,6 +71,7 @@ describe('Handle request to generate', () => { (report) => new Report({ ...report, _index: '.reporting-foo-index-234' }) ), } as unknown as ReportingStore); + mockRequest = getMockRequest(); mockResponseFactory = getMockResponseFactory(); @@ -80,6 +81,7 @@ describe('Handle request to generate', () => { mockContext = getMockContext(); mockContext.reporting = {} as ReportingSetup; + requestHandler = new RequestHandler( reportingCore, { username: 'testymcgee' }, @@ -195,7 +197,6 @@ describe('Handle request to generate', () => { "output": Object {}, "payload": Object { "browserTimezone": "UTC", - "indexPatternSavedObject": undefined, "isDeprecated": true, "layout": Object { "id": "preserve_layout", 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/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 && ( - + )} diff --git a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx index 6a146882fbab5..cb36aff214a76 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx @@ -269,7 +269,12 @@ describe('graph controls: when relsover is loaded with an origin node', () => { .testSubject('resolver:graph-controls:node-legend:description') .map((description) => description.text()) ) - ).toYieldEqualTo(['Running Process', 'Terminated Process', 'Loading Process', 'Error']); + ).toYieldEqualTo([ + 'Running Process', + 'Terminated Process', + 'Loading Process', + 'Error Process', + ]); }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx index 96a59383b1a4e..570f444814d7f 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx @@ -569,7 +569,7 @@ const NodeLegend = ({ > {i18n.translate('xpack.securitySolution.resolver.graphControls.errorCube', { - defaultMessage: 'Error', + defaultMessage: 'Error Process', })} 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); 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/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(), 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('') - ); - } -}; 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/stack_alerts/server/plugin.test.ts b/x-pack/plugins/stack_alerts/server/plugin.test.ts index b2bf076eaf49d..b9263553173d2 100644 --- a/x-pack/plugins/stack_alerts/server/plugin.test.ts +++ b/x-pack/plugins/stack_alerts/server/plugin.test.ts @@ -11,8 +11,7 @@ import { alertsMock } from '../../alerting/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks'; import { BUILT_IN_ALERTS_FEATURE } from './feature'; -// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699 -describe.skip('AlertingBuiltins Plugin', () => { +describe('AlertingBuiltins Plugin', () => { describe('setup()', () => { let context: ReturnType; let plugin: AlertingBuiltinsPlugin; 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: { 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" } } }, diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx index df4c73908b627..425f35b067026 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx @@ -8,15 +8,17 @@ import React, { useContext, useEffect, useState } from 'react'; import useIntersection from 'react-use/lib/useIntersection'; import styled from 'styled-components'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; + import { isScreenshotImageBlob, isScreenshotRef, ScreenshotRefImageData, -} from '../../../../../../common/runtime_types/ping'; +} from '../../../../../../common/runtime_types'; import { useFetcher, FETCH_STATUS } from '../../../../../../../observability/public'; import { getJourneyScreenshot } from '../../../../../state/api/journey'; import { UptimeSettingsContext } from '../../../../../contexts'; + import { NoImageDisplay } from './no_image_display'; import { StepImageCaption } from './step_image_caption'; import { StepImagePopover } from './step_image_popover'; @@ -129,9 +131,12 @@ export const PingTimestamp = ({ label, checkGroup, initialStepNo = 1 }: Props) = )} - - {label} - + + {label && ( + + {label} + + )} ); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx index a2858348ed59c..3fa94e45f8937 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx @@ -6,10 +6,13 @@ */ import React, { MouseEvent, useEffect } from 'react'; -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { nextAriaLabel, prevAriaLabel } from './translations'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText, useEuiTheme } from '@elastic/eui'; + import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { ScreenshotRefImageData } from '../../../../../../common/runtime_types'; +import { useBreakpoints } from '../../../../../hooks'; + +import { nextAriaLabel, prevAriaLabel } from './translations'; export interface StepImageCaptionProps { captionContent: string; @@ -23,13 +26,6 @@ export interface StepImageCaptionProps { isLoading: boolean; } -const ImageCaption = euiStyled.div` - background-color: ${(props) => props.theme.eui.euiColorLightestShade}; - display: inline-block; - width: 100%; - text-decoration: none; -`; - export const StepImageCaption: React.FC = ({ captionContent, imgRef, @@ -41,6 +37,9 @@ export const StepImageCaption: React.FC = ({ label, onVisible, }) => { + const { euiTheme } = useEuiTheme(); + const breakpoints = useBreakpoints(); + useEffect(() => { onVisible(true); return () => { @@ -49,8 +48,10 @@ export const StepImageCaption: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const isSmall = breakpoints.down('m'); + return ( - { // we don't want this to be captured by row click which leads to step list page evt.stopPropagation(); @@ -59,8 +60,9 @@ export const StepImageCaption: React.FC = ({
{(imgSrc || imgRef) && ( - + ) => { setStepNumber(stepNumber - 1); @@ -74,10 +76,11 @@ export const StepImageCaption: React.FC = ({ - {captionContent} + {captionContent} - + ) => { setStepNumber(stepNumber + 1); @@ -93,8 +96,21 @@ export const StepImageCaption: React.FC = ({ )} - {label} + + {label} +
-
+ ); }; + +const CaptionWrapper = euiStyled.div` + background-color: ${(props) => props.theme.eui.euiColorLightestShade}; + display: inline-block; + width: 100%; + text-decoration: none; +`; + +const SecondaryText = euiStyled(EuiText)((props) => ({ + color: props.theme.eui.euiTextColor, +})); diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.test.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.test.tsx index 26ca69a5b89c7..7aa763c15ca1f 100644 --- a/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.test.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.test.tsx @@ -199,6 +199,22 @@ describe('useExpandedROw', () => { expect(Object.keys(result.current.expandedRows)).toEqual(['0']); }); + it('returns expected browser consoles', async () => { + const { result } = renderHook(() => + useExpandedRow({ + steps: defaultSteps, + allSteps: [...defaultSteps, browserConsoleStep], + loading: false, + }) + ); + + result.current.toggleExpand({ journeyStep: defaultSteps[0] }); + + expect(result.current.expandedRows[0].props.browserConsoles).toEqual([ + browserConsoleStep.synthetics.payload.text, + ]); + }); + describe('getExpandedStepCallback', () => { it('matches step index to key', () => { const callback = getExpandedStepCallback(2); @@ -207,3 +223,44 @@ describe('useExpandedROw', () => { }); }); }); + +const browserConsoleStep = { + _id: 'IvT1oXwB5ds00bB_FVXP', + observer: { + hostname: '16Elastic', + }, + agent: { + name: '16Elastic', + id: '77def92c-1a78-4353-b9e5-45d31920b1b7', + type: 'heartbeat', + ephemeral_id: '3a9ca86c-08d0-4f3f-b857-aeef540b3cac', + version: '8.0.0', + }, + '@timestamp': '2021-10-21T08:25:25.221Z', + package: { name: '@elastic/synthetics', version: '1.0.0-beta.14' }, + ecs: { version: '1.12.0' }, + os: { platform: 'darwin' }, + synthetics: { + package_version: '1.0.0-beta.14', + journey: { name: 'inline', id: 'inline' }, + payload: { + text: "Refused to execute inline script because it violates the following Content Security Policy directive: \"script-src 'unsafe-eval' 'self'\". Either the 'unsafe-inline' keyword, a hash ('sha256-P5polb1UreUSOe5V/Pv7tc+yeZuJXiOi/3fqhGsU7BE='), or a nonce ('nonce-...') is required to enable inline execution.\n", + type: 'error', + }, + index: 755, + step: { duration: { us: 0 }, name: 'goto kibana', index: 1, status: '' }, + type: 'journey/browserconsole', + isFullScreenshot: false, + isScreenshotRef: true, + }, + monitor: { + name: 'cnn-monitor - inline', + timespan: { lt: '2021-10-21T08:27:04.662Z', gte: '2021-10-21T08:26:04.662Z' }, + check_group: '70acec60-3248-11ec-9921-acde48001122', + id: 'cnn-monitor-inline', + type: 'browser', + status: 'up', + }, + event: { dataset: 'browser' }, + timestamp: '2021-10-21T08:25:25.221Z', +}; diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.tsx index e58e1cca8660b..1b3a641033dd7 100644 --- a/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.tsx @@ -28,13 +28,15 @@ export const useExpandedRow = ({ loading, steps, allSteps }: HookProps) => { const { checkGroupId } = useParams<{ checkGroupId: string }>(); - const getBrowserConsole = useCallback( + const getBrowserConsoles = useCallback( (index: number) => { - return allSteps.find( - (stepF) => - stepF.synthetics?.type === 'journey/browserconsole' && - stepF.synthetics?.step?.index! === index - )?.synthetics?.payload?.text; + return allSteps + .filter( + (stepF) => + stepF.synthetics?.type === 'journey/browserconsole' && + stepF.synthetics?.step?.index! === index + ) + .map((stepF) => stepF.synthetics?.payload?.text!); }, [allSteps] ); @@ -48,7 +50,7 @@ export const useExpandedRow = ({ loading, steps, allSteps }: HookProps) => { expandedRowsN[expandedRowKey] = ( @@ -77,7 +79,7 @@ export const useExpandedRow = ({ loading, steps, allSteps }: HookProps) => { [stepIndex]: ( diff --git a/x-pack/plugins/uptime/public/components/synthetics/executed_step.test.tsx b/x-pack/plugins/uptime/public/components/synthetics/executed_step.test.tsx index 04fcf382fd861..f9876593a03db 100644 --- a/x-pack/plugins/uptime/public/components/synthetics/executed_step.test.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/executed_step.test.tsx @@ -71,14 +71,15 @@ describe('ExecutedStep', () => { }); it('renders accordions for console output', () => { - const browserConsole = - "Refused to execute script from because its MIME type ('image/gif') is not executable"; + const browserConsole = [ + "Refused to execute script from because its MIME type ('image/gif') is not executable", + ]; const { getByText } = render( - + ); expect(getByText('Console output')); - expect(getByText(browserConsole)); + expect(getByText(browserConsole[0])); }); }); diff --git a/x-pack/plugins/uptime/public/components/synthetics/executed_step.tsx b/x-pack/plugins/uptime/public/components/synthetics/executed_step.tsx index add34c3f71f0d..57b94544e5983 100644 --- a/x-pack/plugins/uptime/public/components/synthetics/executed_step.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/executed_step.tsx @@ -19,7 +19,7 @@ interface ExecutedStepProps { step: JourneyStep; index: number; loading: boolean; - browserConsole?: string; + browserConsoles?: string[]; } const Label = euiStyled.div` @@ -40,12 +40,7 @@ const ExpandedRow = euiStyled.div` width: 100%; `; -export const ExecutedStep: FC = ({ - loading, - step, - index, - browserConsole = '', -}) => { +export const ExecutedStep: FC = ({ loading, step, index, browserConsoles }) => { const isSucceeded = step.synthetics?.payload?.status === 'succeeded'; return ( @@ -94,7 +89,12 @@ export const ExecutedStep: FC = ({ initialIsOpen={!isSucceeded} > <> - {browserConsole} + {browserConsoles?.map((browserConsole) => ( + <> + {browserConsole} + + + ))} diff --git a/x-pack/plugins/uptime/public/hooks/index.ts b/x-pack/plugins/uptime/public/hooks/index.ts index 3e4714384e654..e96d746a05514 100644 --- a/x-pack/plugins/uptime/public/hooks/index.ts +++ b/x-pack/plugins/uptime/public/hooks/index.ts @@ -12,4 +12,5 @@ export * from './use_search_text'; export * from './use_cert_status'; export * from './use_telemetry'; export * from './use_url_params'; +export * from './use_breakpoints'; export { useIndexPattern } from '../contexts/uptime_index_pattern_context'; diff --git a/x-pack/plugins/uptime/public/hooks/use_breakpoints.test.ts b/x-pack/plugins/uptime/public/hooks/use_breakpoints.test.ts new file mode 100644 index 0000000000000..d417d98dcb76d --- /dev/null +++ b/x-pack/plugins/uptime/public/hooks/use_breakpoints.test.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BREAKPOINTS } from '@elastic/eui'; +import { renderHook } from '@testing-library/react-hooks'; +import { useBreakpoints } from './use_breakpoints'; + +describe('use_breakpoints', () => { + describe('useBreakpoints', () => { + const width = global.innerWidth; + + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + }); + + afterAll(() => { + (global as { innerWidth: number }).innerWidth = width; + }); + + it('should only return up => false and down => true for "xs" when width is less than BREAKPOINTS.xs', () => { + (global as { innerWidth: number }).innerWidth = BREAKPOINTS.xs - 1; + const { result } = renderHook(() => useBreakpoints()); + + expect(result.current.up('xs')).toBeFalsy(); + expect(result.current.down('xs')).toBeTruthy(); + }); + + it('should only return up => true and down => false for "xs" when width is above or equal BREAKPOINTS.xs', () => { + (global as { innerWidth: number }).innerWidth = BREAKPOINTS.xs; + const { result } = renderHook(() => useBreakpoints()); + + expect(result.current.up('xs')).toBeTruthy(); + expect(result.current.down('xs')).toBeFalsy(); + }); + + it('should return down => true for "m" when width equals BREAKPOINTS.l', () => { + (global as { innerWidth: number }).innerWidth = BREAKPOINTS.l; + const { result } = renderHook(() => useBreakpoints()); + + expect(result.current.up('m')).toBeTruthy(); + expect(result.current.down('m')).toBeFalsy(); + }); + + it('should return `between` => true for "m" and "xl" when width equals BREAKPOINTS.l', () => { + (global as { innerWidth: number }).innerWidth = BREAKPOINTS.l; + const { result } = renderHook(() => useBreakpoints()); + + expect(result.current.between('m', 'xl')).toBeTruthy(); + }); + + it('should return `between` => true for "s" and "m" when width equals BREAKPOINTS.s', () => { + (global as { innerWidth: number }).innerWidth = BREAKPOINTS.s; + const { result } = renderHook(() => useBreakpoints()); + + expect(result.current.between('s', 'm')).toBeTruthy(); + }); + + it('should return up => true for all when size is > xxxl+', () => { + (global as { innerWidth: number }).innerWidth = 3000; + const { result } = renderHook(() => useBreakpoints()); + + expect(result.current.up('xs')).toBeTruthy(); + expect(result.current.up('s')).toBeTruthy(); + expect(result.current.up('m')).toBeTruthy(); + expect(result.current.up('l')).toBeTruthy(); + expect(result.current.up('xl')).toBeTruthy(); + expect(result.current.up('xxl')).toBeTruthy(); + expect(result.current.up('xxxl')).toBeTruthy(); + }); + + it('should determine `isIpad (Portrait)', () => { + (global as { innerWidth: number }).innerWidth = 768; + const { result } = renderHook(() => useBreakpoints()); + + const isIpad = result.current.up('m') && result.current.down('l'); + expect(isIpad).toEqual(true); + }); + + it('should determine `isMobile (Portrait)`', () => { + (global as { innerWidth: number }).innerWidth = 480; + const { result } = renderHook(() => useBreakpoints()); + + const isMobile = result.current.up('xs') && result.current.down('s'); + expect(isMobile).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/hooks/use_breakpoints.ts b/x-pack/plugins/uptime/public/hooks/use_breakpoints.ts new file mode 100644 index 0000000000000..9398a5fcd15fe --- /dev/null +++ b/x-pack/plugins/uptime/public/hooks/use_breakpoints.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useState } from 'react'; +import useWindowSize from 'react-use/lib/useWindowSize'; +import useDebounce from 'react-use/lib/useDebounce'; + +import { BREAKPOINTS, EuiBreakpointSize } from '@elastic/eui'; + +// Custom breakpoints +const BREAKPOINT_XL = 1599; // Overriding the theme's default 'xl' breakpoint +const BREAKPOINT_XXL = 1599; +const BREAKPOINT_XXXL = 2000; + +export type BreakpointKey = EuiBreakpointSize | 'xxl' | 'xxxl'; + +type BreakpointPredicate = (breakpointKey: BreakpointKey) => boolean; +type BreakpointRangePredicate = (from: BreakpointKey, to: BreakpointKey) => boolean; + +/** + * Returns the predicates functions used to determine whether the current device's width is above or below the asked + * breakpoint. (Implementation inspired by React Material UI). + * + * @example + * const { breakpoints } = useBreakpoints(); + * const isMobile = breakpoint.down('m'); + * + * @example + * const { breakpoints } = useBreakpoints(); + * const isTablet = breakpoint.between('m', 'l'); + * + * @param debounce {number} Debounce interval for optimization + * + * @returns { {up: BreakpointPredicate, down: BreakpointPredicate, between: BreakpointRangePredicate} } + * Returns object containing predicates which determine whether the current device's width lies above, below or + * in-between the given breakpoint(s) + * { + * up => Returns `true` if the current width is equal or above (inclusive) the given breakpoint size, + * or `false` otherwise. + * down => Returns `true` if the current width is below (exclusive) the given breakpoint size, or `false` otherwise. + * between => Returns `true` if the current width is equal or above (inclusive) the corresponding size of + * `fromBreakpointKey` AND is below (exclusive) the corresponding width of `toBreakpointKey`. + * Returns `false` otherwise. + * } + */ +export function useBreakpoints(debounce = 50) { + const { width } = useWindowSize(); + const [debouncedWidth, setDebouncedWidth] = useState(width); + + const up = useCallback( + (breakpointKey: BreakpointKey) => isUp(debouncedWidth, breakpointKey), + [debouncedWidth] + ); + const down = useCallback( + (breakpointKey: BreakpointKey) => isDown(debouncedWidth, breakpointKey), + [debouncedWidth] + ); + + const between = useCallback( + (fromBreakpointKey: BreakpointKey, toBreakpointKey: BreakpointKey) => + isBetween(debouncedWidth, fromBreakpointKey, toBreakpointKey), + [debouncedWidth] + ); + + useDebounce( + () => { + setDebouncedWidth(width); + }, + debounce, + [width] + ); + + return { up, down, between, debouncedWidth }; +} + +/** + * Returns the corresponding device width against the provided breakpoint key, either the overridden value or the + * default value from theme. + * @param key {BreakpointKey} string key representing the device breakpoint e.g. 'xs', 's', 'xxxl' + */ +function getSizeForBreakpointKey(key: BreakpointKey): number { + switch (key) { + case 'xxxl': + return BREAKPOINT_XXXL; + case 'xxl': + return BREAKPOINT_XXL; + case 'xl': + return BREAKPOINT_XL; + case 'l': + return BREAKPOINTS.l; + case 'm': + return BREAKPOINTS.m; + case 's': + return BREAKPOINTS.s; + } + + return BREAKPOINTS.xs; +} + +function isUp(size: number, breakpointKey: BreakpointKey) { + return size >= getSizeForBreakpointKey(breakpointKey); +} + +function isDown(size: number, breakpointKey: BreakpointKey) { + return size < getSizeForBreakpointKey(breakpointKey); +} + +function isBetween(size: number, fromBreakpointKey: BreakpointKey, toBreakpointKey: BreakpointKey) { + return isUp(size, fromBreakpointKey) && isDown(size, toBreakpointKey); +} 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'), 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/examples/config.ts b/x-pack/test/examples/config.ts index 606f97f9c3de7..cf37cad5bc243 100644 --- a/x-pack/test/examples/config.ts +++ b/x-pack/test/examples/config.ts @@ -33,7 +33,11 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { reportName: 'X-Pack Example plugin functional tests', }, - testFiles: [require.resolve('./search_examples'), require.resolve('./embedded_lens')], + testFiles: [ + require.resolve('./search_examples'), + require.resolve('./embedded_lens'), + require.resolve('./reporting_examples'), + ], kbnTestServer: { ...xpackFunctionalConfig.get('kbnTestServer'), diff --git a/x-pack/test/examples/reporting_examples/capture_test.ts b/x-pack/test/examples/reporting_examples/capture_test.ts new file mode 100644 index 0000000000000..62460bd140bba --- /dev/null +++ b/x-pack/test/examples/reporting_examples/capture_test.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import path from 'path'; +import type { FtrProviderContext } from '../../functional/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'reporting']); + const compareImages = getService('compareImages'); + const testSubjects = getService('testSubjects'); + + const appId = 'reportingExample'; + + const fixtures = { + baselineAPng: path.resolve(__dirname, 'fixtures/baseline/capture_a.png'), + baselineAPdf: path.resolve(__dirname, 'fixtures/baseline/capture_a.pdf'), + baselineAPdfPrint: path.resolve(__dirname, 'fixtures/baseline/capture_a_print.pdf'), + }; + + describe('Captures', () => { + it('PNG that matches the baseline', async () => { + await PageObjects.common.navigateToApp(appId); + + await (await testSubjects.find('shareButton')).click(); + await (await testSubjects.find('captureTestPanel')).click(); + await (await testSubjects.find('captureTestPNG')).click(); + + await PageObjects.reporting.clickGenerateReportButton(); + const url = await PageObjects.reporting.getReportURL(60000); + const captureData = await PageObjects.reporting.getRawPdfReportData(url); + + const pngSessionFilePath = await compareImages.writeToSessionFile( + 'capture_test_baseline_a', + captureData + ); + + expect( + await compareImages.checkIfPngsMatch(pngSessionFilePath, fixtures.baselineAPng) + ).to.be.lessThan(0.09); + }); + + it('PDF that matches the baseline', async () => { + await PageObjects.common.navigateToApp(appId); + + await (await testSubjects.find('shareButton')).click(); + await (await testSubjects.find('captureTestPanel')).click(); + await (await testSubjects.find('captureTestPDF')).click(); + + await PageObjects.reporting.clickGenerateReportButton(); + const url = await PageObjects.reporting.getReportURL(60000); + const captureData = await PageObjects.reporting.getRawPdfReportData(url); + + const pdfSessionFilePath = await compareImages.writeToSessionFile( + 'capture_test_baseline_a', + captureData + ); + + expect( + await compareImages.checkIfPdfsMatch(pdfSessionFilePath, fixtures.baselineAPdf) + ).to.be.lessThan(0.001); + }); + + it('print-optimized PDF that matches the baseline', async () => { + await PageObjects.common.navigateToApp(appId); + + await (await testSubjects.find('shareButton')).click(); + await (await testSubjects.find('captureTestPanel')).click(); + await (await testSubjects.find('captureTestPDFPrint')).click(); + + await PageObjects.reporting.checkUsePrintLayout(); + await PageObjects.reporting.clickGenerateReportButton(); + const url = await PageObjects.reporting.getReportURL(60000); + const captureData = await PageObjects.reporting.getRawPdfReportData(url); + + const pdfSessionFilePath = await compareImages.writeToSessionFile( + 'capture_test_baseline_a', + captureData + ); + + expect( + await compareImages.checkIfPdfsMatch(pdfSessionFilePath, fixtures.baselineAPdfPrint) + ).to.be.lessThan(0.001); + }); + }); +} diff --git a/x-pack/test/examples/reporting_examples/fixtures/baseline/capture_a.pdf b/x-pack/test/examples/reporting_examples/fixtures/baseline/capture_a.pdf new file mode 100644 index 0000000000000..3966d4406b7b2 Binary files /dev/null and b/x-pack/test/examples/reporting_examples/fixtures/baseline/capture_a.pdf differ diff --git a/x-pack/test/examples/reporting_examples/fixtures/baseline/capture_a.png b/x-pack/test/examples/reporting_examples/fixtures/baseline/capture_a.png new file mode 100644 index 0000000000000..7c121804e4296 Binary files /dev/null and b/x-pack/test/examples/reporting_examples/fixtures/baseline/capture_a.png differ diff --git a/x-pack/test/examples/reporting_examples/fixtures/baseline/capture_a_print.pdf b/x-pack/test/examples/reporting_examples/fixtures/baseline/capture_a_print.pdf new file mode 100644 index 0000000000000..9036a87c6397c Binary files /dev/null and b/x-pack/test/examples/reporting_examples/fixtures/baseline/capture_a_print.pdf differ diff --git a/x-pack/test/examples/reporting_examples/index.ts b/x-pack/test/examples/reporting_examples/index.ts new file mode 100644 index 0000000000000..a0e8689a26586 --- /dev/null +++ b/x-pack/test/examples/reporting_examples/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { PluginFunctionalProviderContext } from 'test/plugin_functional/services'; + +// eslint-disable-next-line import/no-default-export +export default function ({ loadTestFile }: PluginFunctionalProviderContext) { + describe('reporting examples', function () { + this.tags('ciGroup13'); + + loadTestFile(require.resolve('./capture_test')); + }); +} 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/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/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/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 0000000000000..76ac07831dec1 Binary files /dev/null and b/x-pack/test/functional/es_archives/ml/categorization_small/data.json.gz differ 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" + } + } + } +} 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(); }, diff --git a/x-pack/test/functional/services/compare_images.ts b/x-pack/test/functional/services/compare_images.ts new file mode 100644 index 0000000000000..9ad98dff3819c --- /dev/null +++ b/x-pack/test/functional/services/compare_images.ts @@ -0,0 +1,149 @@ +/* + * 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 path from 'path'; +import { promises as fs } from 'fs'; +import { pdf as pdfToPng } from 'pdf-to-img'; +import { comparePngs } from '../../../../test/functional/services/lib/compare_pngs'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export function CompareImagesProvider({ getService }: FtrProviderContext) { + const log = getService('log'); + const config = getService('config'); + + const screenshotsDir = config.get('screenshots.directory'); + + const writeToSessionFile = async (name: string, rawPdf: Buffer) => { + const sessionDirectory = path.resolve(screenshotsDir, 'session'); + await fs.mkdir(sessionDirectory, { recursive: true }); + const sessionReportPath = path.resolve(sessionDirectory, `${name}.png`); + await fs.writeFile(sessionReportPath, rawPdf); + return sessionReportPath; + }; + + return { + writeToSessionFile, + async checkIfPngsMatch( + actualPngPath: string, + baselinePngPath: string, + screenshotsDirectory: string = screenshotsDir + ) { + log.debug(`checkIfPngsMatch: ${baselinePngPath}`); + // Copy the pngs into the screenshot session directory, as that's where the generated pngs will automatically be + // stored. + const sessionDirectoryPath = path.resolve(screenshotsDirectory, 'session'); + const failureDirectoryPath = path.resolve(screenshotsDirectory, 'failure'); + + await fs.mkdir(sessionDirectoryPath, { recursive: true }); + await fs.mkdir(failureDirectoryPath, { recursive: true }); + + const actualPngFileName = path.basename(actualPngPath, '.png'); + const baselinePngFileName = path.basename(baselinePngPath, '.png'); + + const baselineCopyPath = path.resolve( + sessionDirectoryPath, + `${baselinePngFileName}_baseline.png` + ); + // Don't cause a test failure if the baseline snapshot doesn't exist - we don't have all OS's covered and we + // don't want to start causing failures for other devs working on OS's which are lacking snapshots. We have + // mac and linux covered which is better than nothing for now. + try { + log.debug(`writeFile: ${baselineCopyPath}`); + await fs.writeFile(baselineCopyPath, await fs.readFile(baselinePngPath)); + } catch (error) { + throw new Error(`No baseline png found at ${baselinePngPath}`); + } + + const actualCopyPath = path.resolve(sessionDirectoryPath, `${actualPngFileName}_actual.png`); + log.debug(`writeFile: ${actualCopyPath}`); + await fs.writeFile(actualCopyPath, await fs.readFile(actualPngPath)); + + let diffTotal = 0; + + const diffPngPath = path.resolve(failureDirectoryPath, `${baselinePngFileName}-${1}.png`); + diffTotal += await comparePngs( + actualCopyPath, + baselineCopyPath, + diffPngPath, + sessionDirectoryPath, + log + ); + + return diffTotal; + }, + async checkIfPdfsMatch( + actualPdfPath: string, + baselinePdfPath: string, + screenshotsDirectory = screenshotsDir + ) { + log.debug(`checkIfPdfsMatch: ${actualPdfPath} vs ${baselinePdfPath}`); + // Copy the pdfs into the screenshot session directory, as that's where the generated pngs will automatically be + // stored. + const sessionDirectoryPath = path.resolve(screenshotsDirectory, 'session'); + const failureDirectoryPath = path.resolve(screenshotsDirectory, 'failure'); + + await fs.mkdir(sessionDirectoryPath, { recursive: true }); + await fs.mkdir(failureDirectoryPath, { recursive: true }); + + const actualPdfFileName = path.basename(actualPdfPath, '.pdf'); + const baselinePdfFileName = path.basename(baselinePdfPath, '.pdf'); + + const baselineCopyPath = path.resolve( + sessionDirectoryPath, + `${baselinePdfFileName}_baseline.pdf` + ); + const actualCopyPath = path.resolve(sessionDirectoryPath, `${actualPdfFileName}_actual.pdf`); + + // Don't cause a test failure if the baseline snapshot doesn't exist - we don't have all OS's covered and we + // don't want to start causing failures for other devs working on OS's which are lacking snapshots. We have + // mac and linux covered which is better than nothing for now. + try { + log.debug(`writeFileSync: ${baselineCopyPath}`); + await fs.writeFile(baselineCopyPath, await fs.readFile(baselinePdfPath)); + } catch (error) { + log.error(`No baseline pdf found at ${baselinePdfPath}`); + return 0; + } + log.debug(`writeFileSync: ${actualCopyPath}`); + await fs.writeFile(actualCopyPath, await fs.readFile(actualPdfPath)); + + const actualPdf = await pdfToPng(actualCopyPath); + const baselinePdf = await pdfToPng(baselineCopyPath); + + log.debug(`Checking number of pages`); + + if (actualPdf.length !== baselinePdf.length) { + throw new Error( + `Expected ${baselinePdf.length} pages but got ${actualPdf.length} in PDFs expected: "${baselineCopyPath}" actual: "${actualCopyPath}".` + ); + } + + let diffTotal = 0; + let pageNum = 1; + + for await (const actualPage of actualPdf) { + for await (const baselinePage of baselinePdf) { + const diffPngPath = path.resolve( + failureDirectoryPath, + `${baselinePdfFileName}-${pageNum}.png` + ); + diffTotal += await comparePngs( + { path: path.resolve(screenshotsDirectory, '_actual.png'), buffer: actualPage }, + { path: path.resolve(screenshotsDirectory, '_baseline.png'), buffer: baselinePage }, + diffPngPath, + sessionDirectoryPath, + log + ); + ++pageNum; + break; + } + } + + return diffTotal; + }, + }; +} diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts index 5e40eb040178b..3e69a5f43928a 100644 --- a/x-pack/test/functional/services/index.ts +++ b/x-pack/test/functional/services/index.ts @@ -61,6 +61,7 @@ import { } from './dashboard'; import { SearchSessionsService } from './search_sessions'; import { ObservabilityProvider } from './observability'; +import { CompareImagesProvider } from './compare_images'; // define the name and providers for services that should be // available to your tests. If you don't specify anything here @@ -112,4 +113,5 @@ export const services = { reporting: ReportingFunctionalProvider, searchSessions: SearchSessionsService, observability: ObservabilityProvider, + compareImages: CompareImagesProvider, }; 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')); }); } 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(); 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); diff --git a/yarn.lock b/yarn.lock index 53ae14a127444..e1a7d3cf3639d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2337,10 +2337,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@37.0.0": - version "37.0.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-37.0.0.tgz#a94526461c404b449953cca4fe34f8bf3620413e" - integrity sha512-Pfm58/voERWVPJlxy13DphwgRoBGYhnSyz65kdsPg6lYGxN5ngWvuTuJ3477fyApYV01Pz4Ckt9yj1BSQue80Q== +"@elastic/charts@38.0.1": + version "38.0.1" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-38.0.1.tgz#9c1db7e0f1de869e0b2b505e192bbb9d62d60dc8" + integrity sha512-i9mIA3Ji9jSjuFDtuh9gV1xpCl3sbBEDgJiOgLVt04pr/qZH2W+tr3AV5yHvjsR7Te0Pmh/Cm5wLBvFKaI1nIA== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" @@ -4090,6 +4090,21 @@ resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.5.0.tgz#f60b6a55a5d8e5ee908347d2ce4250b15103dc8e" integrity sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg== +"@mapbox/node-pre-gyp@^1.0.0": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz#2a0b32fcb416fb3f2250fd24cb2a81421a4f5950" + integrity sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA== + dependencies: + detect-libc "^1.0.3" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.1" + nopt "^5.0.0" + npmlog "^4.1.2" + rimraf "^3.0.2" + semver "^7.3.4" + tar "^6.1.0" + "@mapbox/point-geometry@0.1.0", "@mapbox/point-geometry@^0.1.0", "@mapbox/point-geometry@~0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz#8a83f9335c7860effa2eeeca254332aa0aeed8f2" @@ -10077,6 +10092,15 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001097, caniuse-lite@^1.0.30001109, can resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001261.tgz#96d89813c076ea061209a4e040d8dcf0c66a1d01" integrity sha512-vM8D9Uvp7bHIN0fZ2KQ4wnmYFpJo/Etb4Vwsuc+ka0tfGDHvOPrFm6S/7CCNLSOkAUjenT2HnUPESdOIL91FaA== +canvas@2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/canvas/-/canvas-2.8.0.tgz#f99ca7f25e6e26686661ffa4fec1239bbef74461" + integrity sha512-gLTi17X8WY9Cf5GZ2Yns8T5lfBOcGgFehDFb+JQwDqdOoBOcECS9ZWMEAqMSVcMYwXD659J8NyzjRY/2aE+C2Q== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.0" + nan "^2.14.0" + simple-get "^3.0.3" + capture-exit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" @@ -22292,6 +22316,19 @@ pbkdf2@^3.0.3: safe-buffer "^5.0.1" sha.js "^2.4.8" +pdf-to-img@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pdf-to-img/-/pdf-to-img-1.1.1.tgz#1918738477c3cc95a6786877bb1e36de81909400" + integrity sha512-e+4BpKSDhU+BZt34yo2P5OAqO0CRRy8xSNGDP7HhpT2FMEo5H7mzNcXdymYKRcj7xIr0eK1gYFhyjpWwHGp46Q== + dependencies: + canvas "2.8.0" + pdfjs-dist "2.9.359" + +pdfjs-dist@2.9.359: + version "2.9.359" + resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-2.9.359.tgz#e67bafebf20e50fc41f1a5c189155ad008ac4f81" + integrity sha512-P2nYtkacdlZaNNwrBLw1ZyMm0oE2yY/5S/GDCAmMJ7U4+ciL/D0mrlEC/o4HZZc/LNE3w8lEVzBEyVgEQlPVKQ== + pdfkit@>=0.8.1, pdfkit@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/pdfkit/-/pdfkit-0.11.0.tgz#9cdb2fc42bd2913587fe3ddf48cc5bbb3c36f7de" @@ -27523,7 +27560,7 @@ tar@6.1.9: mkdirp "^1.0.3" yallist "^4.0.0" -tar@^6.0.2, tar@^6.1.11, tar@^6.1.2: +tar@^6.0.2, tar@^6.1.0, tar@^6.1.11, tar@^6.1.2: version "6.1.11" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==