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 (
+
+ );
+};
+
+const testImage = `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vP32O7LIYkuW7tdLVf/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 = ({
-
+
+
+
+
+
+
+
+ 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"
/>
-
-
+
-
-
-
+
+
+
-
+
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==